DOS Handle Capacity: FILES, JFTs, SFTs, and INT 21h AH=67h
Separate DOS per-process handles from the system open-file table, then size FreeDOS FILES and AH=67h without assuming one limit controls every program.
“Too many open files” can mean that a DOS process exhausted its own handle table, the kernel ran out of system-wide open-file slots, or an application runtime imposed a smaller limit before the kernel was reached. These are separate ceilings. Changing FILES= can raise the system table allocation, but it does not automatically enlarge every process’s handle table. Calling INT 21h function 67h can adjust the caller’s per-process table, but it cannot manufacture free kernel slots or override a language runtime’s own limit.
DOS uses a Job File Table (JFT) associated with each process and a system-wide table of open file/device state, commonly called the System File Table (SFT) in technical references. The process-visible handle returned by OPEN or CREATE is an index into the JFT. The JFT entry refers to a system-wide open object record. That separation explains why a process may have an unused handle index but still fail to open a file when the system-wide table is full.
Distinguish per-process and system-wide limits
The MS-DOS Encyclopedia describes two limits for handle-based files. Each process has a table of handles, and DOS maintains an internal table of file and device records shared across active processes. Its historical MS-DOS defaults and maxima vary by DOS version, so those numbers should not be copied into a FreeDOS capacity plan. The FreeDOS help for FILES documents its own directive range and behavior: FILES=nnn or FILESHIGH=nnn, with a documented range of 8 through 255 and a default of 8. The help also warns that other restrictions can prevent a program from opening the configured number.
The practical model is:
application or language runtime limit
↓
process JFT entries (handle numbers visible to the program)
↓
system-wide open file/device table configured by FILES
↓
filesystem, redirector, and device-specific limits
The JFT is the process-facing indirection layer: a small handle value is meaningful in the context of the calling process. DOS can duplicate handles so two handle numbers refer to shared open state, including a shared file position. Consequently, “number of open handles” and “number of independently opened files” are not always the same count. The SFT-side record contains state associated with the open object, while the process table provides the indices an application passes back to DOS. This indirection is why applications must not treat a handle as a permanent global identifier or persist it across process launches.
The standard input, output, error, auxiliary, and printer handles are conventional entries from DOS startup. They occupy handle slots even when a program redirects, closes, or reuses one. A capacity test that starts by opening files without accounting for inherited handles can overstate how many additional slots are available. Conversely, duplicating an existing handle can create another process-visible handle without creating a completely independent file open. Count the resource relevant to the failure.
These are not interchangeable counters. A high-level runtime may keep its own descriptor table and map its descriptor to a DOS handle. A network redirector can have additional state requirements. Device handles may occupy system state even though they do not refer to ordinary disk files. An application that opens many small files can fail earlier than a kernel-level test if its library was built with a low descriptor limit.
FILES configures the kernel-wide pool
FILES= is a startup directive in FDCONFIG.SYS or CONFIG.SYS; it is not a shell variable and does not retroactively change a running kernel. FreeDOS documents that the setting reserves memory for the configured number of concurrently open files and devices. Increase it only after reproducing a real exhaustion condition or confirming a program’s documented requirement. More entries consume memory that is scarce in conventional-memory configurations; FILESHIGH attempts to place the data high when a suitable memory manager is present.
For example, a test profile might use:
FILES=40
That is an illustrative setting, not a recommended universal maximum. Use the exact spelling accepted by the kernel, keep the original configuration, reboot into the test profile, and run the actual workload. Compare memory availability as well as the file-open test. Do not infer that a larger FILES value is harmless just because the parser accepts it.
INT 21h AH=67h changes the caller’s JFT size
The DOS Set Handle Count function receives the requested table size in BX and reports success or failure through the carry flag. In historical DOS documentation, this is a DOS 3.3-and-later interface. It affects the current process’s open-file table, not the kernel-wide count configured by FILES=. An application should check the carry flag and returned error instead of assuming that a request was accepted.
; Illustrative 8086/MASM-style call sequence.
; Request 40 process handles before opening the workload's files.
mov bx, 40
mov ah, 67h
int 21h
jc handle_table_error
The snippet demonstrates the register contract, not a complete program. Preserve registers as required by the language ABI, retain room for standard handles, and treat failure as a real capacity error. Do not make a smaller handle table while high-numbered handles remain open; compatibility behavior varies among DOS implementations. Child-process inheritance also has version-specific rules, so a parent that increases its table must verify whether the child receives the intended handles on the target kernel.
Increasing the JFT does not necessarily increase a C or Pascal runtime’s file descriptor limit. A runtime can reject an open before calling DOS, and a library may not expose DOS handle numbers directly. Diagnose with a minimal assembly-level test only when the application runtime is not the suspected ceiling. For production, test the actual executable and toolchain, not just a micro-test.
The safe order is to request per-process capacity before opening the large working set, check the call result, and then open only the files the application needs. Do not request an enormous table by default: the JFT itself needs storage and its implementation may allocate or relocate memory. Shrinking the table while higher-numbered entries remain open can discard live references or fail in implementation-specific ways. Close handles above the proposed new limit before reducing it, and retest child-process behavior if the parent launches helpers.
If a runtime fails after opening a small fixed number of files while a direct DOS test opens more, raising FILES= is unlikely to fix that runtime ceiling. If several unrelated processes stop at a similar global point, the kernel-wide table is a stronger suspect. If only a network-backed open fails, the redirector may impose additional limits. These are diagnostic patterns, not proofs: confirm them with one-variable tests and record the error returned at the failing operation.
Capacity testing that identifies the failing layer
Use a disposable directory and a program that opens files one at a time, retains each successful handle, and records the first failure. Test the workload in three stages:
- Run under the current boot profile and record the highest simultaneous open count.
- Increase
FILESin a separate boot profile, then repeat without changing the process JFT request. - Request a larger JFT in the application and repeat with the same kernel-wide setting.
If only the FILES change moves the threshold, the system table was likely the limiting layer. If only the JFT call changes it, the per-process table mattered. If neither changes the application threshold but a direct DOS test succeeds farther, inspect the runtime library and application configuration. If the threshold changes when a network redirector or resident driver is loaded, account for that component’s resource use and documented requirements.
Count open state, not the total number of files scanned over the life of a process. A program can process thousands of files successfully if it closes each handle promptly. Conversely, a leak of one handle per record can exhaust a table even when each file is small. Instrument close paths and error cleanup, and confirm that a failed open does not leave a handle allocated.
Failure handling and operational guardrails
The normal DOS open/create interface returns a handle in AX on success and sets the carry flag with an error code on failure. A program should close every successful handle on all exit paths and preserve the first meaningful error before cleanup calls overwrite status. INT 21h function 59h can provide extended error information in compatible DOS environments, but it should be called immediately after the failing operation if the application relies on its result.
Avoid optimizing one table without recording what changed. Store the original and test boot files, kernel version, FILES setting, memory manager, loaded redirectors, runtime library, number of concurrently retained handles, and the observed DOS error. Retest after changing a kernel, redirector, runtime, or application build. The file table is a compatibility boundary, not a single modern operating-system limit.
The safest final configuration is the smallest documented FILES allocation and per-process handle count that passes the full production workload with margin. Verify both the normal path and error cleanup, then reboot into the rollback profile to prove it remains usable. Treat a successful boot or parser message as insufficient evidence: the decisive test is the target application opening, using, and closing its actual working set without data loss or silent descriptor reuse.
Related:
- FreeDOS SHARE: File Sharing Modes and Byte-Range Locks in Real Mode
- DOS INT 21h Function 59h: Read Extended Error Diagnostics
Sources: