iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more
A Python service can remain running with its port in LISTEN and still reset incoming requests if the process has exhausted a resource limit. In this incident, a long-lived local web UI leaked SQLite connections until its process had 255 of 256 allowed file descriptors open. The root cause was a mistaken assumption: with conn: commits or rolls back a transaction, but it does not close a Python sqlite3 connection.
What happened when the service stayed up but requests reset
In a 2026 incident report, ACS Developer described a self-built local web UI with a job queue, running as a macOS LaunchAgent. The process remained alive and its port continued to show LISTEN, but HTTP requests failed with “Connection reset by peer.” The same symptom over a VPN route made a problem isolated to one network path less likely and redirected attention to the service process. Read the incident account.
The author reported 255 open descriptors against a soft maxfiles limit of 256. The reported listing included 122 descriptors associated with queue.db and 121 with queue.db-wal; the rest included other objects such as sockets. Those counts describe this process and its environment, not a standard descriptor cost for SQLite or a universal macOS limit.
A running process and a listening socket establish only that the process and socket still exist. They do not prove the application can accept and serve new work. A process close to its descriptor limit may be unable to open resources needed by a request, even while monitoring still shows it alive.
#1 Best Overall
Why with conn: did not stop the leak
The incident’s helper opened a database connection and returned it. The caller used with db() as conn:, which the author initially understood like with open(...): do the work and close the resource automatically. That assumption was wrong for sqlite3.Connection.
Python’s official sqlite3 documentation says: “The context manager neither implicitly opens a new transaction nor closes the connection.” On leaving the block, it commits an open transaction if the block succeeds or rolls it back if an exception occurs. The connection itself remains open until explicitly closed. In a long-lived service, repeated calls that open connections without closing them can therefore accumulate resources.
Rank #2
SQLite’s write-ahead logging documentation explains why the incident showed both database and WAL filenames: an open connection to a WAL-mode database uses a separate write-ahead log, usually with a -wal suffix. SQLite usually deletes that file when the last connection closes, though it can remain in specified circumstances. Its appearance helps explain the filenames; it does not establish a fixed one-descriptor-per-file or one-to-one descriptor ratio for every connection.
Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Repair Windows errors before they cause bigger problemsFix Now →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →How to close the connection while preserving transaction handling
Choose a cleanup pattern that matches the existing code and makes connection ownership clear. The two approaches supported by the incident and Python’s documentation are explicit closure in a finally block, or contextlib.closing() when a closing context manager is preferable.
Rank #3
| Approach | Connection cleanup | Transaction behavior | When it fits |
|---|---|---|---|
Explicit try/finally with conn.close() |
The finally block runs on normal and exceptional exits, so the connection is closed when the scope ends. |
Manage commit and rollback separately, as required by the application. | Useful when the caller already handles transactions or needs explicit control. This is the pattern the incident author reported adopting. |
contextlib.closing(conn) |
Calls close() when the closing context exits, including on exceptions. |
It does not replace transaction handling; use transaction management separately where needed. | Useful when a closing context manager makes the resource lifetime easier to read. Python’s documentation recommends it when a closing context manager is needed. |
For example, if the caller is responsible for the transaction, an explicit pattern can make both responsibilities visible:
conn = db()
try:
try:
# Perform database work.
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
The example separates transaction outcome from connection lifetime: commit or roll back the work, then close the connection regardless of outcome. Adapt transaction boundaries to the application’s actual needs rather than assuming that closing alone commits or rolls back work.
Rank #4
How to diagnose descriptor exhaustion in a running service
- Inspect the service process itself. Count its open file descriptors and inspect the process-level limit that applies to the LaunchAgent or other service. A limit observed in an interactive shell may not be the limit inherited by the service.
- Identify what is accumulating. Group open descriptors by resource or filename. Many database and WAL entries can point toward connection lifecycle issues; sockets, logs, pipes, and other resources may have different owners and causes.
- Trace resource ownership in code. Find where each connection is created, which function owns it, and where that ownership scope ends. Ensure every path—including exceptions—closes the connection.
- Check adjacent lifecycle issues. The incident author also corrected a separate parent-process log-file descriptor lifecycle issue. Treat that as a finding specific to this system, not a required SQLite fix.
- Repeat the workload and compare counts. Record a baseline, send repeated requests or calls, and compare descriptor counts afterward. A stable count is more informative than a single successful request.
The incident account does not provide a universal command sequence for every macOS version or service manager; use tools appropriate to the affected process and confirm the limit and descriptors for that process rather than assuming a machine-wide value.
What changed after the fix—and what the numbers mean
After adding explicit connection closure and correcting the separate log-file lifecycle issue, the author reported two checks: 300 calls in a test environment increased the descriptor count by three or fewer, and 120 service requests left the reported total at 23 before and after. Following a restart, routes returned HTTP 200 and stored data remained present. These are the author’s reported results, not independently reproduced tests or guarantees for other applications.
Best Value
The useful verification is the pattern: exercise the service repeatedly, then compare its open-resource count before and after. A repaired lifecycle should not show unbounded growth under that workload. If the count still climbs, inspect other descriptor owners rather than assuming SQLite is the only source.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

