# eventsource is a strict EventSource client with a hard 100 MB parser buffer

> The eventsource package is a MIT licensed TypeScript implementation of the WhatWG server-sent events client for Node.js, browsers, Deno, Bun and partly Cloudflare Workers. It aims to implement an absolute minimal amount beyond the specification, adds error codes and a configurable buffer limit, and ships a migration guide for the two previous major versions.

**EventSource/eventsource** — EventSource client for Node.js, browsers and other JavaScript runtimes

- Repository: https://github.com/EventSource/eventsource
- Stars: 1,161 · Forks: 267
- Language: TypeScript
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/eventsource-eventsource

## Minimal by design, and it says so

The eventsource package is described as a WhatWG and W3C compatible server-sent events client, and the README states its design goal in one line: the module attempts to implement an absolute minimal amount of features and changes beyond the specification.

That sentence explains most of what you need to know about the library before you install it. It is not going to add conveniences that the specification does not have. There is no unified event shape, no promise-based API, no reconnect strategy configuration beyond what the spec defines, and no plugin system. What you get is the client the browser would give you, with the pieces that browsers and non-browser runtimes do not agree on filled in.

The package is authored by Espen Hovlandsdal, with Aslak Hellesøy and Einar Otto Stangvik as contributors, and it is MIT licensed. The npm page has a bundlephobia badge, which is a nod to the fact that a library used in a browser bundle is judged on its size, and the weekly downloads badge suggests it is a dependency in a fair number of projects.

The API is the DOM one.

```ts
import {EventSource} from 'eventsource'

const es = new EventSource('https://my-server.com/sse')

es.addEventListener('notice', (event) => {
  console.log(event.data)
})
es.addEventListener('message', (event) => {
  console.log(event.data)
})
```

The message event is documented as a special case, capturing events without an event field as well as events with the specific type event: message, and not triggering on any other event type. That is the specification behaviour, and it is worth internalising before you name your events carelessly, since a server that sends an unnamed event lands in your message handler whether or not that is what you meant.

## The runtime floor is a list of platform features, not a version number

The supported engines list is unusual and more informative than a version table would be. Node.js 22.12 or newer, Chrome 84 or newer, Safari 15 or newer with a non-ASCII event ID limitation, Firefox 105 or newer, Edge 84 or newer, Deno 2 or newer, Bun 1.1.23 or newer, and Cloudflare Workers as partial.

Underneath, the README reduces the requirement to a list of platform features: fetch, ReadableStream, TextDecoder, URL, Event, MessageEvent and EventTarget, private class fields, methods and accessors, and Symbol.for. That is the real compatibility contract. A runtime that has all of those can run the library regardless of what it calls itself.

The consequence is a two-tier support policy. If you need to support runtimes without private class fields, methods and accessors, the README points to the 4.x branch and version range. If you need even older runtimes, try the 2.x branch and version range, with the note that the 2.x branch is primarily targeted at Node.js rather than browsers.

So the version you install is a statement about the platform you are willing to require, and the README makes the fallback lines explicit rather than leaving you to discover them. That is a good approach for a client library that has been around long enough to have accumulated old consumers.

The TypeScript section fits the same pattern. You configure your TSConfig lib to match your target, dom for browsers, and install the matching types package, @types/node at version 22 or higher for Node.js or @cloudflare/workers-types for Workers. The error messages the README lists, Cannot find name Event, EventTarget or MessageEvent, are all the same error, and they all have the same cause, a lib setting that does not include the DOM types.

## Safari double-encodes the Last-Event-ID header

The Safari and WebKit section documents a real interoperability bug that is easy to lose an afternoon over.

Safari and WebKit encode non-ASCII Last-Event-ID header values as UTF-8 a second time, so the server receives a different ID on reconnect. The characters affected are accented characters, currency symbols and emoji. The important qualifier is that event data and MessageEvent.lastEventId retain their original Unicode values, so the corruption is only in the header that goes out on the wire.

The consequence is specific and severe. The Last-Event-ID header is what the client sends when it reconnects, and it is how a server resumes a stream at the right place. If the ID is mangled on the way out, the server resumes at the wrong position, and the symptom is a stream that silently skips or repeats events after a reconnect. Nothing in the protocol tells you the ID was wrong, because from the server's point of view it received a valid string.

The mitigation the README recommends is on the server side: use ASCII-only values in the event stream's id field, for the widest client compatibility. It links the WebKit source, the ResourceRequestCFNet.h header encoding code, as the underlying behaviour.

This is the kind of detail that justifies a client library existing at all. Every application that implements SSE by hand hits this, and the fix is not discoverable from the specification, it is only visible once a Safari user reports a broken stream.

## Cloudflare Workers needs a different event handler style

The Cloudflare Workers support is marked partial, and the README explains exactly which parts are partial and why.

The platform requirement is compatibilityDate '2024-11-11' or later, and the API requirement is addEventListener() instead of the onmessage, onopen and onerror properties. The reason is a workerd behaviour, and the README describes it precisely: workerd's EventTarget dispatches on<type> handler properties itself, on top of the listener the module registers, so a handler assigned that way is called twice per event, and assigning null only removes one of the two registrations, so it does not unsubscribe.

```js
// Called twice per message on Cloudflare Workers
eventSource.onmessage = (event) => console.log(event.data)

// Use this instead
eventSource.addEventListener('message', (event) => console.log(event.data))
```

A double-called handler and a handler that cannot be removed are both worse than a missing feature, so the workaround is mandatory rather than optional on that platform. The linked workerd issue is the reference.

There is a second, subtler Workers problem. workerd also drops origin and lastEventId from the MessageEvent constructor, and this module assigns them itself, so both are correct on Workers today, and the workaround goes away once the linked workerd pull request lands. In other words, the library compensates for a platform bug and will stop compensating when the platform is fixed.

The third one is about encoding. Workers' native fetch encodes header values as UTF-8 itself, so the module passes it the original event ID to avoid encoding twice, and custom fetch implementations receive a UTF-8 byte string. A wrapper that forwards to the native fetch has to decode the Last-Event-ID back to Unicode first, which is a constraint on anyone writing a fetch wrapper for this library on Workers.

## Two extensions, and the buffer limit that will bite you

The README lists extensions to the WhatWG and W3C API, and there are two that matter.

The first is that the error event has a message and a code property, which the specification does not give it.

```ts
es.addEventListener('error', (err) => {
  if (err.code === 401 || err.code === 403) {
    console.log('not authorized')
  }
})
```

That turns an opaque failure into something you can branch on. A 401 or a 403 on an SSE endpoint is a permanent failure, and the plain Event semantics give you an error event with no reason, so you cannot distinguish a bad token from a transient network blip. The code property is what lets a client show the right message.

The second extension is the parser buffer limit. The parser buffers up to 100 MB while waiting for a complete EventSource line, and the limit is configurable through the maxBufferSize constructor option.

```ts
const es = new EventSource('https://my-server.com/sse', {
  maxBufferSize: 10 * 1024 * 1024, // 10 MB
})
```

The behaviour when the limit is exceeded is worth knowing, because it is not a reconnect. The connection fails and emits an error event, and it will not reconnect. That is a deliberate design choice, since a stream that is producing a single 100 MB line without a newline is broken, and retrying it would spin.

The advice the README gives is the right one anyway, and it is a server-side fix rather than a client-side one: prefer servers that emit smaller chunks or newlines more frequently over increasing this limit. The 100 MB default exists to catch a misbehaving server rather than to be a tunable budget.

The third extension, a custom fetch implementation passed in the constructor, is how you set HTTP request headers on the request, which is the usual reason people need it, since the EventSource constructor has no header option of its own.

## Testing across five runtimes, and a build that is just tsc

The test configuration is the most telling thing about how this library is maintained. There are five Vitest configuration files at the root.

```
vitest.config.ts
vitest.browser.config.ts
vitest.happy-dom.config.ts
vitest.ios.config.ts
vitest.workerd.config.ts
```

The default one runs the ordinary Node tests, the browser one runs in a real browser, the happy-dom one runs against a DOM emulation, the workerd one targets the Cloudflare runtime, and the ios one runs against an iOS simulator, with a separate boot script that starts the simulator. That is a testing matrix that matches the supported engines list, which is the right approach for a library whose whole job is behaving identically across those environments.

The iOS simulator is there because of the Safari section above. The WebKit Last-Event-ID bug can only be reproduced on WebKit, so the project runs its tests in a WebKit environment to catch it, which is a level of rigour that most client libraries do not reach.

The build itself is deliberately plain. The build script is tsc -p tsconfig.dist.json, the lint script runs oxlint and two tsc noEmit passes over the main and test configurations, the formatter is oxfmt rather than prettier, and the package is type module with sideEffects set to false. The types field points at dist/index.d.ts and the exports map has a source condition pointing at src/index.ts, so a bundler with source support can consume the TypeScript directly.

The release process uses changesets, with a changeset script and a release script that publishes the changesets, and the publishConfig enables provenance for the npm publication, which is the npm attestations feature. The repository also has a CHANGELOG.md, a MIGRATION.md with a v2 to v3 guide, a SECURITY.md, a CODE_OF_CONDUCT.md and a CONTRIBUTING.md, and the presence of bun.lock, deno.lock and package-lock.json shows that three package managers are being kept working.

## Where eventsource is the wrong choice

The first wrong case is when you want a streaming API that is not SSE. The README points at the eventsource-client package for a modern alternative with a less constrained API, and that is the honest recommendation. If you need to send structured objects, control the backpressure, or have a promise-based interface, the strict spec compliance of this library is in the way rather than an asset.

The second wrong case is bidirectional streaming. Server-sent events is a one-way channel. The client sends a request, the server pushes events, and the only data the client can send afterwards is whatever the protocol allows, which is nothing. If your use case needs the client to talk back mid-stream, this is the wrong transport and no amount of client library quality fixes it.

The third wrong case is a runtime below the floor. If you have to support a browser from before Chrome 84 or a Node.js older than 22.12, the 5.x line will not run, and the fallbacks are the 4.x and 2.x lines, which are older code that will not receive the Safari fix, the Cloudflare fix or the error code extension. That is a real trade, since the library's own guidance is to pick the version that matches your platform, and the newest version is where the fixes are.

The fourth wrong case is a Node.js service that could use a plain HTTP stream. A server that pushes newline delimited JSON over a chunked response is simpler, has no reconnection semantics to get wrong, and is trivially observable with ordinary HTTP tooling. SSE earns its keep with its reconnection and Last-Event-ID behaviour, and if you do not need that behaviour, the client library is solving a problem you do not have.

## Conclusion

eventsource fits a project that wants the server-sent events client to behave exactly as the specification describes, especially in a Node.js or edge runtime where the built-in browser EventSource does not exist, and it fits anyone who needs the reconnect and Last-Event-ID semantics to be reliable rather than approximated. It does not fit a team that wants a richer streaming API, since the README itself points to the eventsource-client package for a less constrained API. Before adopting it, decide whether your runtime supports the required platform features, since the 5.x line needs private class fields and Symbol.for and older runtimes need the 4.x or 2.x lines, note that the newest release is v5.1.3 from 2026-10-14, and read the Cloudflare Workers notes if you plan to run there, because onmessage handlers are called twice on that platform.

## FAQ

### How do I install the eventsource client?

Run npm install --save eventsource. The package is MIT licensed, is published as the eventsource module, and requires Node.js 22.12 or newer, or a runtime with fetch, ReadableStream, TextDecoder, URL, the Event classes, private class fields and Symbol.for.

### What Node.js and browser versions does eventsource support?

Node.js 22.12 or newer, Chrome 84 or newer, Safari 15 or newer, Firefox 105 or newer, Edge 84 or newer, Deno 2 or newer, Bun 1.1.23 or newer, and Cloudflare Workers as partial. Runtimes without private class fields should use the 4.x branch, and older runtimes the 2.x branch.

### How do I get the HTTP status code from an EventSource error?

The error event carries a message and a code property beyond the specification, so you can check for 401 or 403 to detect an authorisation failure. The README shows the pattern with addEventListener('error', ...) and a check on err.code.

### Why does my SSE stream break on Safari?

Safari and WebKit encode non-ASCII Last-Event-ID header values as UTF-8 a second time, so the server receives a different ID on reconnect. The README recommends that servers use ASCII-only values in the event stream's id field for the widest client compatibility.

### What is the maxBufferSize option for in eventsource?

The parser buffers up to 100 MB while waiting for a complete EventSource line, and maxBufferSize in the constructor options changes that limit. If the limit is exceeded the connection fails and emits an error event, and it will not reconnect, and the README advises preferring servers that emit smaller chunks or more frequent newlines over raising the limit.

## Sources

- [EventSource/eventsource on GitHub](https://github.com/EventSource/eventsource)
- [Issues](https://github.com/EventSource/eventsource/issues)
- [License: MIT](https://github.com/EventSource/eventsource/blob/main/LICENSE)
- [README](https://github.com/EventSource/eventsource/blob/main/README.md)
- [Releases](https://github.com/EventSource/eventsource/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/eventsource-eventsource
