2026-08-07

A 2 GB/s XML Parser for Delphi and Free Pascal: SAX, Streaming and DOM Without the Usual Trade-offs

XML may no longer be fashionable, but it is still everywhere.

Scientific datasets, configuration files, industry standards, legacy systems, government data, document formats and countless integration interfaces continue to produce XML. And when XML documents become large, the choice of parser can make a surprisingly large difference.

For mORMot 2, we have recently written a new XML parser, TXmlParser, with a slightly different goal from the traditional XML libraries found in the Delphi and Free Pascal ecosystems. It started from an AI-driven pull request from zen010101, then it was fully rewritten by hand for performance and enhanced to support several API modes.

Instead of choosing between a low-level SAX parser and a convenient DOM parser, the same parser provides several levels of access:

  • a zero-allocation SAX/pull API;
  • a streaming/navigation API for selecting only the parts of the document that matter;
  • a hybrid SAX/DOM API, where selected subtrees can be materialized as TDocVariantData;
  • and a full XML-to-TDocVariant DOM representation, when that is what the application actually needs.

The interesting part is that the performance remains very high at all these levels.

Using the 24 MB nasa.xml reference document from the University of Washington XML repository, the raw parser reaches about 2.1 GB/s, while the full DOM conversion reaches about 424 MB/s. Selective streaming reaches about 1.6 GB/s, and once the DOM exists, traversing it through the new TDocVariantData.Product() enumerator reaches about 7 GB/s.

There is also an interesting memory optimization: enabling dvoInternNames reduces the complete DOM footprint from approximately 60 MB to 40 MB, with essentially no performance penalty.

The complete benchmark is shown below.

Introducing TXmlParser

The mormot.core.fmt.pas TXmlParser record operates directly on an in-memory UTF-8 buffer. Its basic token API returns element starts and ends, attributes, text, CDATA, comments and processing instructions.

It deliberately remains a relatively small XML parser rather than attempting to implement every XML feature ever standardized. In particular, it does not support DTD processing or namespace URI binding, and it implements only the XPath-like navigation that is useful for its intended role. The parser nevertheless verifies element nesting and reports malformed XML with detailed error information.

The parser is zero-allocation by design: the TXmlParser type is a record, statically allocated on stack, and token names and values normally point directly into the original UTF-8 input buffer. In pure SAX/pull mode, it consumes only 2KB of stack space.

The benchmark begins by loading the complete XML file into a RawUtf8 string:

var x: TXmlParser; // stack-allocated, no x.Free needed

u := StringFromFile('nasa.xml');
x.Init(u, []);
while x.ParseNext <> xtEof do ; // warmup
writeln('warmup to last position = ', KB(x.Position));

The warmup reaches the end of the input:

warmup to last position = 23.8 MB

So the reference document processed by the benchmark is approximately 23.8 MB.

1. Raw SAX/pull mode

The first test deliberately uses the lowest-level API.

// raw SAX/pull mode API
for i := 1 to 4 do
begin
  t.Start;
  x.Rewind;
  while x.ParseNext <> xtEof do ;
  writeln('TXmlParser.ParseNext: ',t.Stop,' ',
    KB(t.PerSec(length(u))),'/s');
end;

This is essentially the simplest possible parser benchmark: rewind to the beginning and ask ParseNext to walk through the entire XML document until xtEof.

There is no DOM creation and no application-level processing.

The measured results were:

TXmlParser.ParseNext: 11.38ms 2 GB/s
TXmlParser.ParseNext: 11.33ms 2 GB/s
TXmlParser.ParseNext: 11.09ms 2.1 GB/s
TXmlParser.ParseNext: 11.02ms 2.1 GB/s

So the parser processes this 23.8 MB XML document in roughly 11 milliseconds, corresponding to approximately 2.0--2.1 GB/s of input throughput.

For applications doing their own SAX-style processing, this is the fast path.

2. Streaming API with ForEach

The next test uses the higher-level streaming API.

The objective is to find every:

datasets/dataset/tableHead/fields/field

and inspect its units value.

The benchmark is:

// Streaming API
n := 0;
t.Start;
if x.Rewind.Find('datasets') then
  while x.ForEach('dataset', 0) do
    while x.ForEach('tableHead', 1) do
      while x.ForEach('fields', 2) do
        while x.ForEach('field', 3) do
          if x.Find('units') and
             x.ConsumeText(s) and
             (s = 'arcsec') then
            inc(n);
writeln('ForEach: n=',n,' in ',t.Stop,' ',
  KB(t.PerSec(length(u))),'/s');

The result is:

ForEach: n=2770 in 14.23ms 1.6 GB/s

This is approximately 1.6 GB/s while actually navigating the XML hierarchy, finding the relevant elements, consuming their text and comparing it with arcsec.

This is particularly interesting because it is much closer to a real application workload than the raw ParseNext benchmark.

The code expresses the structure of the XML directly:

while x.ForEach('dataset', 0) do
  while x.ForEach('tableHead', 1) do
    while x.ForEach('fields', 2) do
      while x.ForEach('field', 3) do
        ...

The parser remains streaming. No DOM representing the complete 24 MB document is created.

Note the weird 0,1,2,3 numbers as second parameter to every ForEach() call: they are how the method identifies each loop and maintains its state, without the need of a more verbose Open/Close methods.

3. Full DOM with TDocVariant

Sometimes streaming is not what an application needs. Perhaps the XML document is reasonably small, or many different parts of it need to be accessed repeatedly. In that case, constructing a DOM-like representation can be much more convenient. The parser therefore supports consuming an element subtree directly into TDocVariantData.

The corresponding benchmark is:

// single DOM using the TDocVariant.Product() enumerator
t.Start;
if x.Rewind.Find('datasets') then // or XmlToVariant()
  x.Consume(ds);
writeln('Consume All: in ',t.Stop,' ',
  KB(t.PerSec(length(u))),'/s');

The result from the updated run is:

Consume All: in 56.28ms 424.4 MB/s

So building the complete TDocVariant representation takes about 56 ms, corresponding to approximately 424 MB/s.

This is naturally slower than raw parsing. The raw ParseNext() test merely walks through the input. Consume() has to construct an in-memory document representation: objects, arrays, fields and values.

The mapping follows a simple and natural convention:

  • XML attributes become object members prefixed with @
  • Text content becomes #text only when mixed with attributes or child elements
  • Repeated sibling elements automatically become JSON arrays
  • By default, all values are stored as "json text" unless xpoVariantGuessType is defined, and numbers/booleans are recognized

For example:

<book id="123">
  <title>mORMot</title>
</book>

becomes:

{
  "@id": "123",
  "title": "mORMot"
}

Mixed content:

<title lang="en">XML</title>

becomes:

{
  "@lang": "en",
  "#text": "XML"
}

xpoVariantGuessType mode:

<invoice id="123">
  <total>12.75</total>
</invoice>

becomes:

{
  "@id": "123",
  "total": 12.75
}

So the 424 MB/s figure measures something substantially more useful than simple XML scanning: it measures how quickly the complete dynamic representation can be constructed.

4. Memory consumption and string interning

Performance is only one part of DOM design.

A DOM has to store the document representation in memory, and XML documents often repeat the same element and attribute names thousands or millions of times. For this reason, TDocVariantData provides the dvoInternNames option. With the default options, consuming the NASA document into TDocVariantData uses approximately 60 MB of memory.

Using:

x.Consume(ds, JSON_XML + [dvoInternNames]);

reduces the memory consumption to approximately 40 MB.

That is roughly a one-third reduction in memory usage, despite the fact that the original XML document itself is about 23.8 MB. More importantly, this reduction does not come with a meaningful performance penalty.

The benchmark without interning is:

// single DOM using the TDocVariant.Product() enumerator
t.Start;
if x.Rewind.Find('datasets') then // or XmlToVariant()
  x.Consume(ds);
writeln('Consume All: in ',t.Stop,' ',
  KB(t.PerSec(length(u))),'/s');

for i := 1 to 2 do
begin
  n := 0;
  t.Start;
  for a in ds.Product('dataset.tableHead.fields.field') do
    if a^.U['units'] = 'arcsec' then
      inc(n);
  writeln('Product: n=',n,' in ',t.Stop,' ',
  KB(t.PerSec(length(u))),'/s');
end;

t.Start;
ds.Clear;
writeln('Clear Consume All: in ',t.Stop,' ',
KB(t.PerSec(length(u))),'/s');

The results are:

Consume All: in 56.28ms 424.4 MB/s
Product: n=2770 in 3.48ms 6.6 GB/s
Product: n=2770 in 3.24ms 7.2 GB/s
Clear Consume All: in 20.73ms 1.1 GB/s

Now compare this with string interning:

// single DOM using the TDocVariant.Product() enumerator and string interning
t.Start;
if x.Rewind.Find('datasets') then // or XmlToVariant()
  x.Consume(ds, JSON_XML + [dvoInternNames]);
writeln('Consume All Interning: in ',t.Stop,' ',
  KB(t.PerSec(length(u))),'/s');

for i := 1 to 2 do
begin
  n := 0;
  t.Start;
  for a in ds.Product('dataset.tableHead.fields.field') do
    if a^.U['units'] = 'arcsec' then
      inc(n);
  writeln('Product: n=',n,' in ',t.Stop,' ',
  KB(t.PerSec(length(u))),'/s');
end;

t.Start;
ds.Clear;
writeln('Clear Consume All Interning: in ',t.Stop,' ',
KB(t.PerSec(length(u))),'/s');

The results are:

Consume All Interning: in 58.04ms 411.5 MB/s
Product: n=2770 in 3.25ms 7.1 GB/s
Product: n=2770 in 3.12ms 7.4 GB/s
Clear Consume All Interning: in 16.85ms 1.3 GB/s

The comparison is striking:

Operation Without interning With dvoInternNames
DOM memory ~60 MB ~40 MB
DOM construction 56.28 ms / 424.4 MB/s 58.04 ms / 411.5 MB/s
Product() #1 3.48 ms / 6.6 GB/s 3.25 ms / 7.1 GB/s
Product() #2 3.24 ms / 7.2 GB/s 3.12 ms / 7.4 GB/s
DOM clearing 20.73 ms / 1.1 GB/s 16.85 ms / 1.3 GB/s

In other words, the benchmark shows no significant performance penalty from interning. The DOM construction measurement changes from 56.28 ms to 58.04 ms. That is only about 1.8 ms difference on a 24 MB document. The Product() measurements are effectively indistinguishable, and the second run is actually slightly faster with interning.

The most interesting difference is memory usage: approximately 60 MB versus 40 MB. There is also less data to clear, which is reflected in the measured destruction time dropping from about 20.7 ms to 16.9 ms.

This makes dvoInternNames a particularly powerful option for large or repetitive XML documents. String interning is especially useful when the document contains many repeated element or attribute names. Instead of keeping separate copies of identical names throughout the dynamic object tree, the representation can share the interned name. The result is a useful combination: less memory, less cleanup work, and essentially the same execution speed. For applications processing large numbers of XML documents, this can be more important than a few percent of raw parsing throughput.

5. TDocVariantData.Product() traversal

After constructing the DOM, the benchmark traverses it twice.

Without interning, the measurements were:

Product: n=2770 in 3.48ms 6.6 GB/s
Product: n=2770 in 3.24ms 7.2 GB/s

With interning:

Product: n=2770 in 3.25ms 7.1 GB/s
Product: n=2770 in 3.12ms 7.4 GB/s

Obviously this does not mean that the program is reparsing XML at 7 GB/s. The XML has already been converted into the TDocVariant representation.

What this measures is the cost of traversing that representation through the Product() enumerator. For such task, the for .. in ... enumerator syntax offered by modern pascal is really powerful. There is no memory allocation involved: each item is retrieved on demand, and returned as convenient PDocVariantData reference.

The expression:

ds.Product('dataset.tableHead.fields.field')

provides a convenient projection over the matching elements.

The application can therefore write:

for a in ds.Product('dataset.tableHead.fields.field') do
  if a^.U['units'] = 'arcsec' then
    inc(n);

The traversal itself is therefore extremely cheap compared with constructing the DOM.

6. Hybrid streaming + partial DOM

The final benchmark combines the two approaches. There is no reason to build a complete 24 MB DOM if only selected parts of the document need convenient DOM-style processing.

The benchmark therefore streams through the outer structure and consumes only the relevant fields subtree:

// hybrid Streaming + partial DOM mode
n := 0;
t.Start;
if x.Rewind.Find('datasets') then
  while x.ForEach('dataset', 0) do
    while x.ForEach('tableHead', 1) do
      while x.Next('fields') do
      begin
        x.Consume(field);
        for a in field.Product('field') do
          if a^.U['units'] = 'arcsec' then
            inc(n);
      end;
writeln('ForEach + Consume Field: n=',n,' in ',t.Stop,' ',
  KB(t.PerSec(length(u))),'/s');

The result:

ForEach + Consume Field: n=2770 in 30.87ms 773.8 MB/s

So the complete operation runs at approximately 774 MB/s.

This is a useful middle ground:

  • the complete XML document is never represented as a DOM;
  • the parser streams through the outer structure;
  • only the relevant fields sections are materialized;
  • those temporary DOM fragments can then be processed with Product().

Finally, the temporary DOM is cleared:

t.Start;
field.Clear;
writeln('Clear last Consume: in ',t.Stop,' ',
  KB(t.PerSec(length(u))),'/s');

Result:

Clear last Consume: in 3us 7.5 TB/s

The apparent 7.5 TB/s figure is obviously not a meaningful XML throughput number. The operation is only three microseconds, so expressing that tiny operation against the original 23.8 MB input produces an artificially enormous normalized bandwidth.

7. XPath //field lookup

The parser also supports a simple XPath-like lookup syntax, which makes it possible to express the same search more directly.

For this NASA document, looking up all <field> elements with:

// XPath //field lookup
n := 0;
t.Start;
x.Rewind;
while x.Find('//field') do
begin
  x.Consume(Field);
  if Field.U['units'] = 'arcsec' then
    inc(n);
end;
writeln('XPath //field lookup: n=',n,' in ',t.Stop,' ',
  KB(t.PerSec(length(u))),'/s');

gives:

XPath //field lookup: n=2770 in 27.46ms 869.7 MB/s

So the XPath-like '//field' lookup processes the complete 23.8 MB document at approximately 870 MB/s while finding the same 2,770 matching elements. Of course, if some <field>...</field> nodes would have appeared outside the regular dataset/tableHead/fields/field location, it would have returned some false positives.

Each x.Find('//field') lookup is very fast, but x.Consume(Field) computes the DOM of each <field> content, so it is slower than the explicit nested ForEach() streaming traversal, which reaches 1.6 GB/s. But it provides a concise way to express a document-wide lookup, with some core nodes DOM representation. The result is also still comfortably below 30 ms for the complete document.

This illustrates another useful point of the API: the parser does not force a choice between maximum streaming performance and convenient path-based navigation. Explicit ForEach() is available when every bit of performance matters, while the XPath-like //field syntax can make less performance-critical queries considerably simpler.

The XPath support of Find() is limited to the 90% useful patterns: /root/catalog calls Rewind to search from the document <root>, catalog/book search nested <catalog><book> from the current position, //book path will find <book> anywhere from the current position. There is no XPath //book/title, predicates, wildcards, attributes or namespaces support by design.

Complete benchmark results

The measurements can now be summarized as follows:

API Time Throughput Matches / memory
ParseNext ~11.0--11.4 ms 2.0--2.1 GB/s -
ForEach streaming 14.23 ms 1.6 GB/s 2,770
Full Consume 56.28 ms 424.4 MB/s ~60 MB
Full Consume + dvoInternNames 58.04 ms 411.5 MB/s ~40 MB
Product() 3.48 / 3.24 ms 6.6 / 7.2 GB/s 2,770
Product() + interning 3.25 / 3.12 ms 7.1 / 7.4 GB/s 2,770
Clear full DOM 20.73 ms 1.1 GB/s * ~60 MB
Clear interned DOM 16.85 ms 1.3 GB/s * ~40 MB
ForEach + partial Consume 30.87 ms 773.8 MB/s 2,770
Clear partial DOM 3 us 7.5 TB/s * -
XPath //field lookup 27.46 ms 869.7 MB/s 2,770 matches

* These throughput figures are useful mainly for normalizing the measured time against the 23.8 MB input. They should not be interpreted as parser bandwidth.

The most meaningful numbers are therefore:

API / operation Throughput
Raw SAX/pull ~2.1 GB/s
Streaming ForEach ~1.6 GB/s
Streaming + partial DOM ~774 MB/s
Full DOM construction ~424 MB/s
Full DOM + interning ~412 MB/s
DOM Product traversal ~7 GB/s
DOM memory with interning ~60 MB → ~40 MB
XPath //field lookup ~870 MB/s

Why the hybrid model matters

XML libraries have traditionally forced developers toward one of two programming models.

The first is SAX-style processing. SAX is efficient because it processes the XML sequentially rather than constructing an entire tree. The downside is that application code must understand the event stream and maintain enough state to reconstruct the logical structure it is interested in.

The second is DOM. DOM is much easier to navigate because the entire XML document becomes a tree. But constructing and retaining that tree consumes memory and introduces allocation and object-management overhead.

TXmlParser tries to make the compromise part of the API itself.

Raw UTF-8 buffer
      |
      +-- ParseNext()
      |      raw SAX/pull
      |
      +-- Find()/Next()/ForEach()
      |      selective streaming
      |
      +-- Consume()
      |      partial DOM
      |
      +-- XmlToVariant()
             complete DOM
                    |
                    +-- Product()
                         efficient traversal

These are not four unrelated XML implementations. They are different views over the same parser state and data representation.

How this compares with existing XML solutions

There is already no shortage of XML support in Pascal. Free Pascal's fcl-xml, for example, provides XML DOM and reader functionality, and even its xpath unit. There are also established SAX interfaces for Pascal, as well as other XML libraries such as OXml that provide combinations of DOM and SAX functionality. On Delphi, developers can also use platform XML implementations such as MSXML, or bindings to native XML engines such as libxml2. Therefore, the benchmark should not be presented as a claim that TXmlParser is universally faster than every existing XML library. That would require a proper cross-library benchmark using the same XML corpus, CPU, operating system, compiler and optimization settings, with equivalent parser features and equivalent workloads. Endless apples-to-oranges comparison for sure.

An interesting comparison may be the numbers published in NesLib XML DOM blog article. Reducing each node to 8 bytes is impressive, and mORMot DOM via TDocVariant uses more memory for sure. But the blog article, and associated benchmark code was in fact plain wrong about its query tests: they parsed for matching units=arcsec but only for the first appearance in each loop, so they identified only 3 and not 2770 occurrences. It is a good indication that, when mining XML data, the API itself is more important than raw performance or memory consumption. Our nested ForEach() loops or our Product() iterator are simple to understand and very effective to use. They found what they are supposed to with no surprise.

The benchmark presented here demonstrates something narrower and more relevant to the mORMot toolbox:

A native Delphi/Free Pascal XML parser can provide very high raw parsing performance while simultaneously offering convenient selective streaming and dynamic DOM APIs without requiring separate parsing engines.

There are extremely mature XML implementations in C and C++, Java, .NET and many other ecosystems. Some are highly optimized, some prioritize complete XML standards compliance, and some deliberately restrict their feature set to maximize performance. Historical XML parser benchmarks have repeatedly shown the fundamental difference between SAX-style processing and DOM construction: sequential event processing can be substantially cheaper than building a complete object tree.

The real advantage is not the 2 GB/s number. The raw number is impressive, but it is not actually the most important part of the benchmark. The interesting result is the range of abstraction levels.

If maximum speed is needed:

while x.ParseNext <> xtEof do
  ...

gives direct access to the parser stream.

If the application needs to locate selected elements:

x.Find(...)
x.ForEach(...)

provides navigation without building the whole document.

If only a particular subtree needs convenient dynamic access:

x.Consume(field);

creates a temporary TDocVariantData.

And if the entire document should simply become a dynamic object:

XmlToVariant(...)

provides the convenient high-level representation.

Then Product() makes querying the resulting document particularly concise:

for a in ds.Product('dataset.tableHead.fields.field') do
  if a^.U['units'] = 'arcsec' then
    inc(n);

And there are also some other TDocVariantData methods accepting returning a PVariant or directly a RawUtf8, for even cleaner code:

var v: PVariant;
    for v in ds.ProductValue('dataset.tableHead.fields.field.units') do
      if VariantEquals(v^, 'arcsec') then // faster than naive v^ = 'arcsec'
        inc(n);
  // ProductValue v^=arcsec: n=2770 in 5.27ms 4.4 GB/s
  // ProductValue VariantEquals: n=2770 in 2.92ms 7.9 GB/s

var s: RawUtf8;
    for s in ds.ProductU('dataset.tableHead.fields.field.units') do
      if s ='arcsec' then
        inc(n);
  // ProductU: n=2770 in 3.49ms 6.6 GB/s

Finally, dvoInternNames allows the same dynamic representation to use substantially less memory when XML names are repeated.

Just compare with the standard Delphi-XML way of computing the very same request:

var
  Dataset, TableHead, Fields, Field, Units: IXMLNode;
  I, J, K, L, N: Integer;

N := 0;
Datasets := XMLDoc.DocumentElement.ChildNodes.FindNode('datasets');
if Datasets <> nil then
  for I := 0 to Datasets.ChildNodes.Count - 1 do
  begin
    Dataset := Datasets.ChildNodes[I];
    if Dataset.NodeName <> 'dataset' then
      Continue;
    for J := 0 to Dataset.ChildNodes.Count - 1 do
    begin
      TableHead := Dataset.ChildNodes[J];
      if TableHead.NodeName <> 'tableHead' then
        Continue;
      for K := 0 to TableHead.ChildNodes.Count - 1 do
      begin
        Fields := TableHead.ChildNodes[K];
        if Fields.NodeName <> 'fields' then
          Continue;
        for L := 0 to Fields.ChildNodes.Count - 1 do
        begin
          Field := Fields.ChildNodes[L];
          if Field.NodeName <> 'field' then
            Continue;
          Units := Field.ChildNodes.FindNode('units');
          if (Units <> nil) and (Units.Text = 'arcsec') then
            Inc(N);
        end;
      end;
    end;
  end;

Which one do you find more natural and less error prone?
Which one do you guess is the fastest and uses less memory?

The design of our TXmlParser was to optimize the common cases:

  • UTF-8 input;
  • sequential parsing;
  • simple element navigation;
  • selective subtree consumption;
  • dynamic variant DOM representation;
  • none or low allocation;
  • efficient string interning;
  • straightforward error handling.

This is a very mORMot-like approach: provide a small, composable primitive that is fast enough for demanding workloads and convenient enough that application developers do not need to abandon the abstraction to get performance.

2026-07-06

Object Pascal Is Still a Serious Contender for High-Performance REST Servers

In this article, we will deep-dive into the architecture that lets our Open Source mORMot 2 library compete with (and sometimes outperform) nginx, Kestrel, and raw C++ implementations.

The library offers several HTTP servers.
The mormot.net.server unit contains the historical mORMot 1 web servers - mainly THttpServer (thread-pool for HTTP/1.0 short requests + one thread per kept-alive HTTP/1.1 connection), and THttpApiServer (using the efficient http.sys kernel API on Windows).
But since mORMot 2, our mormot.net.async unit is our genuine cross-platform, modern async implementation.

Continue reading

2026-06-02

TPipeStream: A High-Performance In-Memory Pipe for Delphi and FPC

The latest version of mORMot 2 introduces TPipeStream, a new TStream descendant designed to efficiently transmit data between two threads.

At first glance, the class looks deceptively simple: one thread writes data using Write(), another thread reads it using Read().
Behind this familiar interface lies a fast, lock-protected ring buffer with blocking semantics, making it suitable for streaming large amounts of data between independent producer and consumer threads.

Continue reading

2026-05-19

Delphi Linux & macOS Support for mORMot 2

We are pleased to share that mORMot 2 now offers initial compatibility with Delphi's Linux and macOS x86_64 compilers for console/server applications.
This effort originated from GitHub issue #442.

Thanks to community testing and contributions, core units now compile and run on these platforms.

Continue reading

2026-03-03

Rencontre entre Pascaliens à Nantes

Si vous êtes dans l'Ouest de la France, vous êtes les bienvenus pour une escale Delphi/Lazarus le jeudi 9 avril fin d'après midi à Nantes!

J'y serais avec toute l'équipe dev de Tranquil IT pour partager des idées, et certainement quelques petites choses plus solides (ou liquides) ! :)

Continue reading

2026-01-14

mORMot 2 Generics Scalability

In the world of Delphi development, generics have become a cornerstone for writing flexible, type-safe code. However, as projects grow in complexity—with hundreds or even thousands of generic specializations—the scalability of these implementations can become a critical bottleneck.
If you're dealing with large-scale applications, these insights could save you hours of build time and frustration.

Continue reading

2026-01-12

New mORMot 2.4 Release

After more than one year since our latest release, it is time for a new release!

Continue reading

2025-11-09

Sneak Peek mORMot 2.4: SCRAM-MCF REST Authentication

With the upcoming 2.4 release of our framework, we completed its REST authentication process, to follow state of the art principles like MCF strong hashing and SCRAM mutual authentication, with secure password storage on the server side.

This blog article will detail the background information needed to identify what is really going on.

Continue reading

- page 1 of 52