Net-Base Magazine

23.08.2026

Hunting memory leaks: Using FastMM FullDebugMode strategically and interpreting stack traces correctly

FastMM FullDebugMode is one of the most effective tools against memory leaks in Delphi projects — but only if it is deliberately enabled, the reports are interpreted correctly, and common false assumptions are avoided. This practical article demonstrates a clean workflow from...

23.08.2026

From magazine topic to project implementation

Relevant service and technical pages for this post

If a Delphi application in production slowly „bloats“, intermittently crashes with Access Violations, or becomes unstable after days of runtime, it is often not a single bug but a pattern: memory is allocated but not cleanly freed — or it is freed too early and later still accessed. This is exactly where FastMM FullDebugMode is invaluable. Not as a permanent state, but as a targeted diagnostic tool that turns „somewhere in the heap something is broken“ back into a traceable cause.

The catch: FullDebugMode produces a lot of output, costs performance and quickly leads to misinterpretation. A leak report does not automatically identify the location of „the bug.“ And a stack trace is only as good as symbol resolution (MAP file, debug info, inlining). In this article I go through the typical edge case, explain the clean approach and the pitfalls — so that in the end you not only find leaks, but fix them sustainably.

When FastMM FullDebugMode is truly useful

FastMM is often already the default memory manager in modern Delphi versions or is integrated into many projects anyway. The FullDebugMode, however, is a special configuration: it marks memory blocks with additional check patterns, collects allocation stack traces and checks more aggressively for heap corruption (i.e. corrupted heap metadata, e.g. due to buffer overruns).

I use FullDebugMode deliberately when one of the following scenarios applies:

  • Reproducible leak: memory usage increases in a test run per operation (e.g. per request, per import, per UI action).
  • Sporadic AVs: especially those that occur „sometimes here, sometimes there“ in the same area (classically: use-after-free).
  • Heap corruption: messages like „Invalid pointer operation“, „Access violation in ntdll“ or crashes during shutdown/finalization.
  • Regression investigation: sudden new instability after refactoring, a library update or a compiler change.

FullDebugMode is not sensible as a „let’s enable it in all builds“ setting. The overhead is high, timing changes, and race conditions in particular can disappear or shift because of it. For continuous operation, a leaner monitoring approach is more suitable (e.g. process working set, private bytes, counters per operation) — FullDebugMode is the scalpel, not the pulse sensor.

Basic principle: leak report is a symptom, the stack trace is a lead

A leak report first shows you: these blocks are still allocated at program end. That is only automatically a problem if these blocks should actually have been freed. There are legitimate „leaks“: global singletons, caches, OS handles with process lifetime or third-party libraries that intentionally do not finalize. You want to know these cases, but not blindly „fix“ them.

The stack trace in the report shows the place where the block was allocated. That is often not the location where you „forgot to Free.“ Common reality in mature systems:

  • Allocation in the UI or service layer, release should happen in a lower layer (ownership unclear).
  • Allocation in a factory, ownership is transferred to the caller — but the caller assumes it is still „owned“.
  • Objects are held in collections (lists, dictionaries), but the ownership model is not consistent.
  • An exception path skips cleanup because try/finally is missing or starts too late.

The proper sequence is therefore: reproduceisolateresolve the stack traceidentify the ownership errorfix with a regression test. FastMM provides the traces, but you must map them to architecture and lifecycles.

Activate FastMM FullDebugMode cleanly (without overlooking side effects)

Schematic graphic showing heap blocks and guard bands converted into a leak report
Abstract: FullDebugMode works with additional guard bands and report output.

In practice FullDebugMode is enabled via the FastMM options and an appropriate FastMM configuration. What matters less is “what exactly the include file is called” and more what the configuration does and under which build conditions you use it.

Recommended conditions for the debug build

  • Debug DCUs and debug information: Stacktraces are only useful if they can be resolved to an actual unit/line/address. Ensure that debug information is generated and that a MAP file is available.
  • Choose optimization consciously: For stacktrace readability an unoptimized build is usually better. Inlining and aggressive optimization can „blur“ stack frames.
  • Same runtime conditions: Use as similar data, configuration and privileges as possible. Many leaks are data-dependent (e.g. rare formats, special paths).
  • Separate 64-bit vs. 32-bit: Memory behavior, alignment and third-party libraries differ. Debug on the target platform where the problem occurs.

One point that admins and technical leads often underestimate: FullDebugMode can also change timing. If threading is involved, race conditions can manifest differently. Therefore it is sensible to also run, in parallel, a run without FullDebugMode that only confirms reproduction. FullDebugMode is then the diagnostic step.

Beware of „ReportMemoryLeaksOnShutdown“

Delphi can report leaks at program termination via ReportMemoryLeaksOnShutdown. That is convenient, but in complex applications (services, plug-in hosts, long runtimes) it can be misleading: during shutdown finalization sections run, threads stop, caches are cleaned up. A leak that is critical in mid-runtime may disappear by the end — or conversely: an apparent leak may only arise during shutdown because background work is still running.

For practical leak hunting it is therefore more important to measure leak per operation (e.g. after 100 requests), not just at termination. FastMM can help with that, but the test setup must reflect it.

The typical edge case: leak report shows „some object“, but the cause is ownership

A classic in enterprise applications: an import process creates auxiliary objects per record (e.g. StringLists, JSON parsers, temporary lists). In the happy path they are released cleanly. In rare cases (validation-driven skip, exception, early exit) an object sticks around. After 10,000 records that becomes visible.

FastMM FullDebugMode helps here because it shows the allocation site. But the “fix” is not “free at the allocation site”. The fix is a robust ownership pattern:

  • Who creates an object is not automatically the owner.
  • Ownership must be clear in the API contract (parameters/return, documentation, naming conventions).
  • Collections must be unambiguous: owning vs. non-owning. Mixed forms will backfire.
  • Exception paths need early try/finally blocks.

If all you see in the stack trace is „TStringList.Create“, the information is not worthless — but it only tells you: something is created here. The question is: where should it end? Architectural thinking helps there more than debugger acrobatics.

Reading stack traces correctly: what you can actually infer from them

Detailaufnahme einer Debugging-Analyse mit unscharfem Debugger und handnotierter Call-Chain
For a stack trace the call chain matters — not the single line.

A stack trace from FastMM is typically a list of return addresses that — with debug symbols — map to units, procedures and ideally line numbers. When you read it, three things are decisive:

  • Top-of-stack is not always the fault: The top frames are often the memory manager/RTL. It gets interesting where your code starts.
  • Call chain instead of single line: The line is only a point. The chain shows which path led to the allocation.
  • Multiple identical blocks: If FastMM reports several leaks of the same size, that is often a recurring path. That’s good: you have reproducibility.

If line numbers are missing: MAP file, packages, release DCUs

Many teams stumble here: FullDebugMode is active, the leak report arrives, but instead of unit/line there are only addresses or cryptic symbols. Typical causes:

  • No MAP file or debug info was generated.
  • You are running against Release-DCUs or third-party DLLs without symbols.
  • The application uses runtime packages: then parts of the code are in BPLs and symbol resolution must match that.
  • Optimization/inlining has made the stack trace harder to read.

In practice this means: for leak hunting you need a build that is deliberately „diagnosis-capable.“ That is a different goal than „as fast as possible.“ Technical leads should treat that as a separate build profile so that not every team member changes project options ad hoc.

Evaluating frames: the “interesting” frame is often one line up

An example from real life (without concrete customer code): the stack trace shows you a routine „LoadConfig“ as the first frame in your code. You see an object creation there. You add a call to Free, the leak is gone — and suddenly it crashes elsewhere with a double free. Why? Because „LoadConfig“ puts the object into a cache, and another code path is already the owner and cleans it up later.

The correct reading would have been: the stack trace shows you where the block is created. The fix is often in the definition: who owns the object after return? If you don’t answer that question cleanly, you only change the failure mode (leak → Access Violation).

Heap corruption vs. leak: Why FullDebugMode often finds the real culprit

Graphic showing a buffer overrun that spills into an adjacent memory region
Heap corruption often appears delayed — FullDebugMode makes it visible earlier.

Many so-called „leaks“ are actually secondary effects: a buffer overrun corrupts heap metadata, the memory manager cannot free cleanly later, and in the end you see seemingly random leaks or invalid pointer operations. FullDebugMode is powerful here because it uses check patterns and performs additional validations on Free/Reuse.

It is important to distinguish:

  • Leak: a block was allocated and never released. Stability degrades over time; a crash is not guaranteed.
  • Use-after-free: a block is freed but used later. Leads to sporadic Access Violations (AVs) that are hard to reproduce.
  • Double Free: a block is freed twice. May crash immediately or only later (if the block was reused in the meantime).
  • Heap corruption: someone writes beyond the boundaries of a block. Symptoms often appear delayed.

FullDebugMode is especially valuable when you see delayed symptoms. The additional validation makes errors visible earlier — often exactly at the point of the incorrect access, not minutes later in an arbitrary Free.

Approach in projects: reproducible leak hunting instead of „debugging in the fog“

If you want to hunt memory leaks, you need a process that is repeatable and sharable within teams. I like to work with a fixed diagnostic framework:

1) Reproduction in a deterministic scenario

Define a test sequence that reliably demonstrates the leak: „Start the service, process 500 messages, stop the service“ or „Open dialog X, perform action Y 200 times.“ It is important to document the sequence with parameters (dataset, tenant, feature flags) so others can reproduce it.

2) Minimize: make the leak visible per step

If the sequence takes 20 minutes, split it. The goal is to be able to compare „before“ and „after“ as quickly as possible. In large applications this is often the real time sink, not the fixing.

3) Enable FullDebugMode and interpret the report

This is where FastMM FullDebugMode comes into play. Collect the reports, group them by block size/callstack and look for repetitions. A single remaining block can be a legitimate cache. 10,000 identical blocks are almost always a real leak.

4) Clarify ownership and fix in the appropriate layer

Fix leaks where ownership is defined: factory, API contract, collection wrapper. Inserting a quick Free directly next to Create is often the wrong place if the object is passed on.

5) Regression: same sequence, same build, same report

A fix is only good when the sequence runs again and neither leaks nor new memory errors occur. Especially with use-after-free, a “leak gone” is not proof but only a new symptom.

Typical pitfalls in Delphi-code that FastMM makes visible

Collections and ownership (lists, dictionaries, interfaces)

Many leaks do not come from complicated algorithms but from everyday data structures. Two classic error patterns:

  • A list contains objects, but no one knows who frees them. Solution: use an owning list or consistently clear in a finally block.
  • A dictionary holds objects as values; on Remove the value is not freed or it is forgotten on Clear.

Interfaces are additionally tricky: reference counting (ARC-like) is convenient, but mixed operation with object ownership can produce leaks in the presence of cyclical references or events. FullDebugMode often shows you the allocation path, but the root cause is a reference cycle (A holds B via interface, B holds A via callback).

Exceptions and early exits

In mature business software systems exceptions are often part of normal control flow (e.g. validation, abort, retry). The problem is rarely the exception itself, but the path around it: an object is created before the try/finally, then an exception is thrown and cleanup is skipped. FullDebugMode gives you the allocation stacktrace — you must then check whether there is a guaranteed release path.

Threads and lifetime: “releasing in the wrong thread”

With VCL/FMX and services using worker threads another edge case appears: an object is created in one thread but freed in the UI thread (or vice versa) because something is “quickly” queued/synchronized across. That can work, but it can also lead to use-after-free if the producer continues working while the consumer already frees the object.

FastMM FullDebugMode can help here because it detects time-shifted errors earlier. The actual fix, however, is a clean lifetime model: clear ownership, transfer only via immutable data or explicit ownership-transfer points.

How to make reports useful: filter, compare, document

In teams it pays off to treat leak reports not just as something to “look at” but as an artifact. Three pragmatic measures that have proven effective:

  • Baseline report: Capture a “known state” (e.g. the current product version) once with FullDebugMode and store it as a reference. That way you spot new leaks immediately.
  • Comparison by use case: For critical workflows (import, export, API request, UI bulk operation) define a short, repeatable sequence for each and run it regularly.
  • Documented “legitimate leaks”: If a cache is intentionally not finalized, document it. Otherwise someone will chase the same entries again in six months.

This is not bureaucracy; it is a time saver: leak hunting otherwise quickly becomes a never-ending loop because the same patterns recur in every sprint.

When the effort pays off — and when you should take a different approach

FastMM FullDebugMode is a diagnostic tool with costs. The effort is particularly justified when:

  • The application runs for a long time (service, terminal-server client, shift-based system, 24/7 processes).
  • You process real customer data streams and cannot cover all paths in tests.
  • Stability is more important than short-term feature velocity (typical for process-centric software solutions).

If, by contrast, you only have a small desktop helper that terminates again after 30 seconds, leak hunting is often of secondary importance. Likewise: if you have a one-off memory spike issue (e.g. a large export), it is often not a leak but a matter of streaming strategy and peak heap load.

Practical conclusion: FullDebugMode is not a switch, it is a process

FastMM FullDebugMode brings structure to memory error diagnosis: it makes allocations visible, detects heap corruption earlier and provides stack traces that let you fix the cause instead of the symptom. However, the decisive lever is not the tool but the procedure: reproducible scenarios, diagnosable builds, clear ownership contracts and regression against a baseline.

If you are stuck on a stubborn leak or sporadic heap error and want to stabilize the issue sustainably in a larger Delphi system, a short, clean diagnostic setup with a clear sequence and analyzable reports is worthwhile. If you need support with analysis, build profiles or architecture refactoring: contact the Net-Base Software GmbH.

Also important for this topic are Delphi Finding Memory Leaks and Reading FastMM Leak Reports. The article places these aspects in context and shows what matters in everyday practice.

Discuss a project or modernization initiative with Net-Base.

Next step

When the topic becomes an actual project, architecture, existing systems and operations should be considered together from the outset.

We support not only with individual issues, but also when source snippets, legacy topics, or portal ideas are to be turned into a robust enterprise project.

  • Current state, target state and technical risks are assessed jointly.
  • REST, data access, portals and rollout are not deferred to a later stage as secondary consequences.
  • You can see early on which path is economically and operationally viable.

Share post

Share this post directly

LinkedIn, X, XING, Facebook, WhatsApp and e-mail are available immediately. For Instagram we will prepare the link and short text directly.

Email

Instagram opens in a new tab. The link and short text are copied to the clipboard beforehand.