Erlang/OTP 29.0 Ships Native Records, Post-Quantum TLS, and a Secure-by-Default SSH Daemon
The May 2026 major release hardens the BEAM runtime's security posture with quantum-resistant cryptography defaults and disabled SSH shell access, while adding experimental native records to the language for the first time.
Overview
Erlang/OTP 29.0 shipped on May 13, 2026, bringing what the project describes as “a new major release with new features, improvements as well as a few incompatibilities,” according to the official announcement from Erlang/OTP maintainer Henrik Nord. The release is headlined by three interlocking themes: a significant tightening of the runtime’s default security posture, the first experimental introduction of native records as a true data type, and a wave of new compiler warnings designed to catch unsafe coding patterns before they reach production.
What We Know
Security: Post-Quantum Crypto and a Locked-Down SSH Daemon
The most operationally significant change in OTP 29 is the hardening of the SSH daemon. According to the official highlights blog, shell, exec, and SFTP services now require explicit enablement via options like {shell, {shell, start, []}}, reducing the default attack surface. The project’s rationale is straightforward: under the previous defaults, any authenticated SSH user could execute arbitrary Erlang code on the server unless administrators had explicitly locked things down. The announcement frames this as implementing the “secure by default” principle, preventing authenticated users from executing arbitrary Erlang code unless explicitly configured.
On the cryptographic side, both SSL and SSH now prefer ML-KEM-768 combined with X25519 for key exchange, protecting against classical and quantum attacks, according to the highlights blog. In SSL, the post-quantum hybrid algorithm x25519mlkem768 is now the most preferred key exchange group in the default configuration, according to the announcement. The GitHub release notes also document post-quantum support in TLS through ML-DSA and SLH-DSA algorithms.
A quieter but meaningful security change: the current working directory (.) has been moved from first to last position in the default code path. The highlights blog explains the rationale — the previous behavior meant corrupt or modified BEAM files placed in the working directory could shadow OTP modules and cause the system to misbehave.
OTP 29 also ships new secure coding guidelines documentation, with the highlights blog noting that new and updated guides provide hardening strategies for TLS connections, inets applications, and SSH services.
Language: Native Records (Experimental) and Multi-Valued Comprehensions
For the first time in Erlang’s history, the runtime ships a native record data type. According to the announcement, native records as described in EEP-79 have been implemented. The announcement provides a precise characterization: “A native record is a data structure similar to the traditional tuple-based records, except that is a true data type.”
Traditional Erlang records have always been compile-time syntactic sugar over tuples — a macro-like transformation that disappears at the BEAM level. Native records are a first-class runtime type with their own identity. The highlights blog notes they use syntax like -record #vec{x=0.0, y=0.0}., are private to their module by default, and can be exported using -export_record() directives. The feature remains experimental in OTP 29 and 30, with potential behavioral changes still possible. The announcement explicitly flags that native records “are considered experimental in Erlang/OTP 29 and possibly also in Erlang/OTP 30.”
Multi-valued comprehensions, specified in EEP 78, also land in this release. The highlights blog illustrates the feature with a concrete example: [I, -I || I <- lists:seq(1, 5)] produces [1,-1,2,-2,3,-3,4,-4,5,-5], generating two elements per iteration without needing nested comprehensions or explicit concatenation.
A third language addition is the compr_assign experimental feature, which allows variable binding within comprehensions. The highlights blog gives the example [H || E <- List, H = erlang:phash2(E), H rem 10 =:= 0], noting it must be enabled via -enable-feature compr_assign or -feature(compr_assign, enable). The GitHub release notes confirm this as an experimental feature enabling variable binding in comprehensions.
The release also adds left-associative function application, allowing f(X)(Y) instead of (f(X))(Y), according to the GitHub release notes.
A new guard BIF, is_integer/3, simplifies range checking. The highlights blog shows the before-and-after: instead of writing is_integer(C) andalso $0 =< C andalso C =< $9, developers can now write is_integer(C, $0, $9).
Compiler Warnings and the -unsafe Attribute
OTP 29 introduces the -unsafe attribute for marking functions as unsafe to use. According to the announcement, the compiler will by default now generate warnings for calls to functions in Erlang/OTP known to be always unsafe. The highlights blog gives a concrete example: the compiler warns about unsafe functions like public_key:decrypt_public/2 and potentially unsafe functions such as those creating new atoms.
Several new compiler warnings ship enabled by default. The highlights blog lists warnings for old-style catch expressions (with a recommendation to use try...catch...end instead), use of and/or operators when enabled via warn_obsolete_bool_op, and variables bound in subexpressions then used externally.
Standard Library: New Modules and Array Overhaul
The standard library gains several new modules. io_ansi adds support for ANSI terminal sequences, enabling colored and styled text output, according to both the announcement and the highlights blog. The ct_doctest module enables testing of documentation examples in module docs using shell syntax, per the highlights blog. A new graph module, described in the highlights blog as a functional alternative to digraph, returns updated graph instances rather than modifying in-place.
The array module gets a substantial overhaul. The highlights blog notes it adds write caching (3x faster sequential writes), along with new functions including prepend/2, append/2, concat/1-2, slice/3, shift/2, and from/2-3. Existing serialized arrays from before OTP 29 are not compatible with the new internal representation, making this a breaking change for any code that serializes arrays with term_to_binary/1.
Two new functions, rand:shuffle/1 and rand:shuffle_s/2, add list randomization to the standard library, according to the highlights blog and the GitHub release notes. The gb_sets:from_ordset/1 and related functions now verify input ordering to prevent invalid data structures, per the highlights blog, which also notes the addition of a new gb_trees:from_list/1 convenience function.
The erl_tar module gains streaming support for large archive entries. According to the highlights blog, it now streams large entries in 64 KB chunks (configurable via {chunks,ChunkSize}), and supports a {max_size,Size} option to prevent disk overflow attacks.
Platform and Deprecation Changes
OTP 29 drops the 32-bit Windows build entirely. According to the announcement, “There is no longer a 32-bit Erlang/OTP build for Windows.”
Looking ahead, the GitHub release notes document the ODBC application and FTP modules as scheduled for removal in OTP 30.
What We Don’t Know
The timeline for native records graduating from experimental status is not specified beyond the note that they may remain experimental through OTP 30. The performance characteristics of native records relative to tuple-based records at scale have not been independently benchmarked. The full migration path for applications using the now-hardened SSH daemon defaults — particularly those that relied on the previous behavior without explicitly configuring shell or exec access — is not detailed in the announcement.
Analysis
OTP 29 is notable less for any single headline feature than for the breadth of its security-first orientation. The combination of post-quantum cryptographic defaults, locked-down SSH, code-path hardening, the new -unsafe attribute machinery, and expanded compiler warnings suggests a coordinated push to make the BEAM runtime harder to misconfigure in production. The Elixir ecosystem, which runs on the same BEAM runtime, will inherit these SSH and crypto changes as Elixir installations upgrade their underlying OTP version.
Native records, meanwhile, represent the kind of long-deferred language-level change that signals maturation rather than revolution. The feature has been proposed and debated for years; shipping it as experimental in OTP 29 starts the clock on a tooling-and-feedback cycle before it stabilizes. How quickly Dialyzer, syntax tools, and major frameworks adopt native record support will determine whether the data type achieves broad uptake or remains a niche alternative to the tuple-record approach that has served the ecosystem for decades.