How Web Crypto API Differs From Node's crypto Module

By Freddy ·

Node.js ships with two separate crypto APIs, and both can do AES encryption, SHA hashing, and key generation. The older one, require('node:crypto'), has been there since Node's early days and wraps OpenSSL directly. The newer one, the Web Crypto API, exposed as globalThis.crypto.subtle in modern Node and as plain crypto.subtle in every browser, was added so the same standards-based API could run in both places. They are not interchangeable drop-ins for each other, and the differences are not just cosmetic.

Two crypto APIs in the same runtime

In a browser, there has only ever been one option: crypto.subtle, standardized by the W3C so that any site could do client-side cryptography without a plugin. Node.js predates that standard by many years and built its own crypto module on top of OpenSSL bindings long before Web Crypto existed anywhere. Once the Web Crypto API stabilized as a cross-runtime standard, Node added an implementation of it too, first as an experimental global and now available directly as globalThis.crypto in current LTS releases. The result is that a Node backend today can reach for either API for the exact same operation, which is convenient for portability but means the two coexist rather than one replacing the other.

Async by design vs sync-first

Every meaningful operation on crypto.subtle, encrypting, hashing, signing, key derivation, returns a Promise, with no synchronous equivalent offered at all. That is a deliberate design choice from the browser side: cryptographic work can be slow enough to block a page's main thread, so the spec forces it off that thread by making every call asynchronous. Node's crypto module takes the opposite default. Most of its functions, createHash, createCipheriv, randomBytes, run synchronously unless you explicitly reach for a callback or promise-based variant like pbkdf2 instead of pbkdf2Sync. Neither approach is wrong, but code written against one will not compile against the other without restructuring the control flow, not just swapping function names.

How you describe an algorithm

Web Crypto asks for an algorithm as a structured object: encrypting with AES-GCM means passing { name: 'AES-GCM', iv } as a parameter, not a string. Node's crypto module uses a single identifier string instead, like 'aes-256-gcm', that encodes the cipher, key size, and mode all at once. Web Crypto also requires an explicit import step before a key can be used at all: raw key bytes have to be passed through crypto.subtle.importKey() and given a usage list (['encrypt', 'decrypt'], for example) before any operation will accept them. Node's module skips that ceremony entirely and lets you hand a raw Buffer straight to createCipheriv.

Opaque keys vs raw buffers

This is the difference with the most real security weight behind it. A Web Crypto key is a CryptoKey object, not a value you can read. Once imported or generated, you cannot log it, serialize it, or inspect its bytes directly, and if it was created with extractable: false, the underlying key material may never leave the crypto implementation at all, not even accidentally through a debugging console.log. Node's crypto module has no equivalent concept: a key is a plain Buffer, printable, serializable, and indistinguishable from any other byte array in your program once it exists. Neither design is unsafe by itself, but Web Crypto's opaque keys make one entire category of mistake, a key ending up in a log line, a stack trace, or an error message, structurally harder to make by accident.

What is actually the same underneath

Despite the different surface, both APIs ultimately call into the same class of underlying primitive implementations, typically OpenSSL or a platform-native equivalent, for a given algorithm. AES-256-GCM encrypted with Web Crypto and AES-256-GCM encrypted with Node's crypto module produce cryptographically equivalent ciphertext for the same key, plaintext, and nonce; the security guarantees of the algorithm itself do not change based on which JavaScript API called into it. The differences covered above are ergonomic and structural, not cryptographic. Picking one over the other is not a security decision the way picking a key size or a cipher mode is.

Which one to actually reach for

In browser code, there is no choice to make: Web Crypto is the only option, since Node's crypto module does not exist client-side. In Node backend code, both are available, and the deciding factor is usually portability and API surface rather than performance. Web Crypto is the better pick when the same code needs to run in the browser, in Node, and in edge runtimes like Deno or Cloudflare Workers without a rewrite, since all of them implement the same standard. Node's crypto module is still the better pick when you need something Web Crypto does not expose at all: synchronous APIs for CPU-bound scripts, direct HMAC helpers, X.509 certificate handling, or older algorithms kept around for interoperability with legacy systems. If you just want to see AES-GCM encryption and decryption working correctly through the actual Web Crypto API, the Encrypt / Decrypt tool on this site runs entirely on crypto.subtle, in your browser, with nothing sent to a server.

← Back to KeyForge