FatFreeFramework¶
One of the first minimalist PHP MCV frameworks. Alternatives are Slim Framework and Leaf PHP.
CLI Routing¶
When writing CLI apps, F3 allows writing in web syntax:
php index.php /my-awesome-route?foo=bar
or in cleaner shell syntax
php index.php my-awesome-route --foo=bar
which will map --foo to $_GET['foo'].
[!info] When using a leading slash in the route, the arguments (
--foo) are not being mapped to the$_GETsuperglobal!php index.php /my-awesome-route --foo=bar
The parsing of CLI routes happens here:
DB\SQL query log — memory bomb in long-running scripts¶
F3's DB\SQL has a built-in SQL profiler that is ON by default. Every executed statement is appended — with interpolated parameters and per-query wall time — to a plain PHP string property ($this->log) on the connection object. Readable via $db->log():
(0.4ms) SELECT * FROM source_reference WHERE source_key='foobar:...'
Why it kills batch jobs¶
Two compounding problems in db/sql.php (exec()):
- Unbounded growth. The log string is never truncated or capped. It lives as long as the connection object — for a web request that's ~100ms and a few KB; for a CLI batch job it grows toward
memory_limitand dies withAllowed memory size ... exhausted. - Quadratic rewrite. The log line is appended before
execute()with a(-0ms)placeholder, then patched afterwards viastr_replace('(-0ms)', '(12.3ms)', $this->log)— over the entire accumulated log string. Every query pays O(total log size). A loop that starts at thousands of queries/second creeps down to a crawl as the string grows (observed: 10k iterations in 33s early → 25min later in the same run).
The OOM stack trace points at db/sql.php line ~234 (the str_replace), which is misleading — the allocation that fails is the log string copy, not your data.
When to turn it off¶
Always, immediately after creating the connection, in any process that runs more than a request's worth of queries:
$db = new DB\SQL('sqlite:data/app.db');
$db->log(false); // disables both the string growth and the rewrite overhead
Applies to: CLI batch jobs, importers/sync scripts, queue/cron workers, migrations, anything with max_execution_time=0. For plain web requests the default is harmless (and $db->log() is occasionally useful for debugging).
There is also a per-call opt-out — $db->exec($sql, $args, $ttl, false) (4th parameter $log) — but the global log(false) is the right default.
[!warning] The framework dates from the single-request-per-process era. Nothing warns you; the profiler silently assumes the process dies before the log matters. In 2026 the F3 default is still ON.
[!note] I reported this myself in January 2018 f3-factory/fatfree-core#321 — "Memory problem with SQL Logs" — JSON import crashed after ~200 of 12,000 items, OOM at sql.php:230. Still open, planned to be fixed in v4. Hit it again in 2026. Undocumented default-on behavior makes it a recurring trap — hence this note.
Same problem class in other frameworks (state: 2026)¶
In-memory query history is a recurring framework foot-gun; F3 is just the worst variant (on by default and quadratic):
| Framework | Mechanism | Default | Notes |
|---|---|---|---|
F3 DB\SQL |
string on connection, str_replace over whole log per query |
ON | $db->log(false) |
| Laravel | DB::enableQueryLog() → array per connection |
off (was ON in Laravel 4 — flipped in L5 for exactly this reason) | Official docs warn the log "is kept in memory" and bulk inserts "can cause the application to use excess memory". Classic OOM in queue workers / Octane when enabled and never flushed; use DB::flushQueryLog() / DB::disableQueryLog(), or DB::listen() for targeted capture. Octane keeps the app in RAM, so anything enabled once leaks across requests. |
| Doctrine DBAL | SQLLogger / DebugStack (array of all queries) |
opt-in | Deprecated in DBAL 3, removed in DBAL 4; replaced by middleware + PSR-3 logger, i.e. stream-out instead of accumulate — the ecosystem's answer to exactly this problem. Symfony's profiler collects queries in dev only. |
Rule of thumb: any ORM/DB layer that offers "get me all executed queries" is accumulating them in RAM somewhere. In long-running processes, find that switch and turn it off.
Sources: Laravel docs — query log kept in memory, excess-memory warning, laravel/framework #737 — request to disable default-on log (L4 era), laravel/framework #1641 — query log memory leak, laravel/framework #56652 — Octane memory on large insertions, Dwight Watson — managing memory issues in Laravel 4, doctrine/orm #10147, doctrine/dbal #4967 — logging middleware replaces SQLLogger, doctrine/dbal #5628