offline

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 $_GET superglobal! 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()):

  1. 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_limit and dies with Allowed memory size ... exhausted.
  2. Quadratic rewrite. The log line is appended before execute() with a (-0ms) placeholder, then patched afterwards via str_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

#php, #fatfreeframework, #f3, #framework, #fat-free-framework
Marcus Obst
Güterweg 89b
09474 Crottendorf
Germany

+49 37344 133407
info@marcus-obst.de