Skip to main content ホーム クリエイター pramoddutta qaskills streaming-api-testing
streaming-api-testing Streaming API testing skill covering Server-Sent Events testing, chunked transfer encoding, gRPC streaming, real-time data validation, backpressure testing, connection resilience, and AI/LLM streaming response testing.
インストールへ移動 Skills Marketplace コミュニティが作成したAIスキルを発見・探索
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
直接コマンドでは確認用 Prompt が省略されます。実行前にソースを確認してください。
npx skills add https://github.com/PramodDutta/qaskills --skill streaming-api-testingコマンドは1行のまま表示されます。コピー前に横へスクロールして全体を確認してください。
ローカルで確認しますか?SkillsMP が現在取得できるファイルをダウンロードできます。
Zipをダウンロード ダウンロード中... このリポジトリの他の Skills Use when publishing SEO blog articles to qaskills.sh, e.g. "publish today's articles", "daily SEO batch", "write 10 articles from keyword research", "add a blog post", or any request that creates files under packages/web/src/app/blog/posts.
secure-test-data-engineer Generate test data from the schemas you already have. Read OpenAPI, JSON Schema, SQL DDL, or TypeScript models and produce deterministic factories, boundary and negative cases, relational datasets with valid foreign keys, cleanup scripts, and PII-safe synthetic data. Production records never leave the machine.
Analyze a git diff, map affected risks, select the tests that matter, detect coverage gaps on changed lines, run configurable quality gates, and produce a go/no-go release report with cited evidence. Recommends only; never merges or deploys.
name Streaming API Testing description Streaming API testing skill covering Server-Sent Events testing, chunked transfer encoding, gRPC streaming, real-time data validation, backpressure testing, connection resilience, and AI/LLM streaming response testing. version 1.0.0 author thetestingacademy license MIT tags ["streaming","sse","server-sent-events","grpc-streaming","real-time","chunked-transfer","ai-streaming","backpressure"] testingTypes ["api","integration","performance"] frameworks ["playwright","jest","vitest"] languages ["typescript","javascript","python"] domains ["api","backend","ai"] agents ["claude-code","cursor","github-copilot","windsurf","codex","aider","continue","cline","zed","bolt","gemini-cli","amp"]
Streaming API Testing Skill
You are an expert software engineer specializing in testing streaming APIs, real-time data protocols, and event-driven architectures. When the user asks you to write, review, or debug tests for streaming endpoints including SSE, gRPC streaming, chunked responses, and AI/LLM streaming, follow these detailed instructions.
Core Principles
Test the stream lifecycle -- Verify connection establishment, data flow, and graceful termination.
Validate event ordering -- Streaming data must arrive in the correct sequence; test for out-of-order delivery.
Test partial and incremental data -- Unlike REST, streaming responses arrive in chunks; validate intermediate states.
Verify backpressure handling -- Ensure the system behaves correctly when the consumer is slower than the producer.
Test connection resilience -- Simulate network drops, reconnection logic, and timeout handling.
Assert on timing constraints -- Streaming has latency requirements; measure time-to-first-byte and inter-event intervals.
Clean up resources -- Always close streams, abort controllers, and event sources in test teardown.
Project Structure
project/
src/
api/
sse-endpoint.ts
grpc-service.ts
chunked-endpoint.ts
llm-stream.ts
tests/
sse/
sse-basic.test.ts
sse-reconnection.test.ts
sse-backpressure.test.ts
grpc/
server-streaming.test.ts
client-streaming.test.ts
bidirectional.test.ts
chunked/
chunked-transfer.test.ts
chunked-json.test.ts
llm/
llm-stream.test.ts
token-validation.test.ts
helpers/
stream-collector.ts
mock-sse-server.ts
mock-grpc-server.ts
timing-utils.ts
vitest.config.ts
SSE Endpoint Testing
Basic SSE Connection and Event Validation
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest' ;
import { createServer, Server } from 'http' ;
import EventSource from 'eventsource' ;
: ;
: ;
( () => {
server = ( {
(req. === ) {
res. ( , {
: ,
: ,
: ,
});
count = ;
interval = ( {
count++;
res. ( );
res. ( );
res. ( );
(count >= ) {
(interval);
res. ();
}
}, );
req. ( , (interval));
}
});
< >( {
server. ( , {
addr = server. ();
( addr === && addr) {
baseUrl = ;
}
();
});
});
});
( {
server. ();
});
( , {
( , () => {
: [] = [];
< >( {
es = ( );
timeout = ( {
es. ();
( ( ));
}, );
es. = {
events. ( . (event. ));
};
es. = {
(timeout);
es. ();
();
};
});
(events). ( );
(events. ( e. )). ([ , , , , ]);
});
( , () => {
response = ( );
(response. . ( )). ( );
(response. . ( )). ( );
(response. . ( )). ( );
response. ?. ();
});
( , () => {
: [] = [];
< >( {
es = ( );
es. = {
eventIds. (event. );
(eventIds. >= ) {
es. ();
();
}
};
es. = {
es. ();
();
};
});
(eventIds). ([ , , , , ]);
});
});
let
server
Server
let
baseUrl
string
beforeAll
async
createServer
(req, res ) =>
if
url
'/events'
writeHead
200
'Content-Type'
'text/event-stream'
'Cache-Control'
'no-cache'
Connection
'keep-alive'
let
0
const
setInterval
() =>
write
`id: ${count} \n`
write
`event: message\n`
write
`data: ${JSON .stringify({ count, timestamp: Date .now() })} \n\n`
if
5
clearInterval
end
100
on
'close'
() =>
clearInterval
await
new
Promise
void
(resolve ) =>
listen
0
() =>
const
address
if
typeof
'object'
`http://localhost:${addr.port} `
resolve
afterAll
() =>
close
describe
'SSE Basic'
() =>
it
'should receive all events in order'
async
const
events
any
await
new
Promise
void
(resolve, reject ) =>
const
new
EventSource
`${baseUrl} /events`
const
setTimeout
() =>
close
reject
new
Error
'Timeout waiting for events'
5000
onmessage
(event ) =>
push
JSON
parse
data
onerror
() =>
clearTimeout
close
resolve
expect
toHaveLength
5
expect
map
(e ) =>
count
toEqual
1
2
3
4
5
it
'should set correct SSE headers'
async
const
await
fetch
`${baseUrl} /events`
expect
headers
get
'content-type'
toBe
'text/event-stream'
expect
headers
get
'cache-control'
toBe
'no-cache'
expect
headers
get
'connection'
toBe
'keep-alive'
await
body
cancel
it
'should include event IDs for resumption'
async
const
eventIds
string
await
new
Promise
void
(resolve ) =>
const
new
EventSource
`${baseUrl} /events`
onmessage
(event ) =>
push
lastEventId
if
length
5
close
resolve
onerror
() =>
close
resolve
expect
toEqual
'1'
'2'
'3'
'4'
'5'
SSE Reconnection Testing
import { describe, it, expect } from 'vitest' ;
import { createServer, Server , IncomingMessage , ServerResponse } from 'http' ;
describe ('SSE Reconnection' , () => {
it ('should reconnect and resume from last event ID' , async () => {
let connectionCount = 0 ;
const server = createServer ((req : IncomingMessage , res : ServerResponse ) => {
connectionCount++;
const lastEventId = req.headers ['last-event-id' ];
res.writeHead (200 , {
'Content-Type' : 'text/event-stream' ,
'Cache-Control' : 'no-cache' ,
Connection : 'keep-alive' ,
});
const startFrom = lastEventId ? parseInt (lastEventId as string ) + 1 : 1 ;
if (connectionCount === 1 ) {
for (let i = startFrom; i <= 3 ; i++) {
res.write (`id: ${i} \ndata: ${JSON .stringify({ n: i })} \n\n` );
}
res.destroy ();
} else {
for (let i = startFrom; i <= 6 ; i++) {
res.write (`id: ${i} \ndata: ${JSON .stringify({ n: i })} \n\n` );
}
res.end ();
}
});
const port = await new Promise <number >((resolve ) => {
server.listen (0 , () => {
const addr = server.address ();
resolve (typeof addr === 'object' ? addr!.port : 0 );
});
});
const allEvents : number [] = [];
await new Promise <void >((resolve ) => {
const es = new EventSource (`http://localhost:${port} /events` );
const timeout = setTimeout (() => {
es.close ();
resolve ();
}, 5000 );
es.onmessage = (event ) => {
const data = JSON .parse (event.data );
allEvents.push (data.n );
if (data.n >= 6 ) {
clearTimeout (timeout);
es.close ();
resolve ();
}
};
});
server.close ();
expect (allEvents).toEqual ([1 , 2 , 3 , 4 , 5 , 6 ]);
expect (connectionCount).toBe (2 );
});
it ('should handle server-sent retry interval' , async () => {
const server = createServer ((req, res ) => {
res.writeHead (200 , {
'Content-Type' : 'text/event-stream' ,
'Cache-Control' : 'no-cache' ,
});
res.write ('retry: 500\n\n' );
res.write ('data: hello\n\n' );
res.end ();
});
const port = await new Promise <number >((resolve ) => {
server.listen (0 , () => {
const addr = server.address ();
resolve (typeof addr === 'object' ? addr!.port : 0 );
});
});
const reconnectTimes : number [] = [];
let lastDisconnect = 0 ;
await new Promise <void >((resolve ) => {
let messageCount = 0 ;
const es = new EventSource (`http://localhost:${port} /events` );
es.onmessage = () => {
messageCount++;
};
es.onerror = () => {
if (lastDisconnect > 0 ) {
reconnectTimes.push (Date .now () - lastDisconnect);
}
lastDisconnect = Date .now ();
if (messageCount >= 2 ) {
es.close ();
resolve ();
}
};
setTimeout (() => {
es.close ();
resolve ();
}, 3000 );
});
server.close ();
if (reconnectTimes.length > 0 ) {
expect (reconnectTimes[0 ]).toBeGreaterThan (400 );
expect (reconnectTimes[0 ]).toBeLessThan (1000 );
}
});
});
Chunked Transfer Encoding Testing
import { describe, it, expect } from 'vitest' ;
import { createServer, Server } from 'http' ;
describe ('Chunked Transfer Encoding' , () => {
let server : Server ;
let baseUrl : string ;
beforeAll (async () => {
server = createServer ((req, res ) => {
if (req.url === '/chunked-json' ) {
res.writeHead (200 , {
'Content-Type' : 'application/json' ,
'Transfer-Encoding' : 'chunked' ,
});
const items = [
{ id : 1 , name : 'first' },
{ id : 2 , name : 'second' },
{ id : 3 , name : 'third' },
];
let index = 0 ;
const sendNext = ( ) => {
if (index < items.length ) {
const prefix = index === 0 ? '[' : ',' ;
const suffix = index === items.length - 1 ? ']' : '' ;
res.write (`${prefix} ${JSON .stringify(items[index])} ${suffix} ` );
index++;
setTimeout (sendNext, 50 );
} else {
res.end ();
}
};
sendNext ();
}
if (req.url === '/ndjson' ) {
res.writeHead (200 , {
'Content-Type' : 'application/x-ndjson' ,
});
const lines = [
{ event : 'start' , ts : 1 },
{ event : 'data' , value : 42 , ts : 2 },
{ event : 'data' , value : 84 , ts : 3 },
{ event : 'end' , ts : 4 },
];
let index = 0 ;
const sendNext = ( ) => {
if (index < lines.length ) {
res.write (JSON .stringify (lines[index]) + '\n' );
index++;
setTimeout (sendNext, 50 );
} else {
res.end ();
}
};
sendNext ();
}
});
await new Promise <void >((resolve ) => {
server.listen (0 , () => {
const addr = server.address ();
baseUrl = `http://localhost:${typeof addr === 'object' ? addr!.port : 0 } ` ;
resolve ();
});
});
});
afterAll (() => server.close ());
it ('should collect chunked JSON array' , async () => {
const response = await fetch (`${baseUrl} /chunked-json` );
const data = await response.json ();
expect (data).toEqual ([
{ id : 1 , name : 'first' },
{ id : 2 , name : 'second' },
{ id : 3 , name : 'third' },
]);
});
it ('should process NDJSON stream line by line' , async () => {
const response = await fetch (`${baseUrl} /ndjson` );
const reader = response.body !.getReader ();
const decoder = new TextDecoder ();
const events : any [] = [];
let buffer = '' ;
while (true ) {
const { done, value } = await reader.read ();
if (done) break ;
buffer += decoder.decode (value, { stream : true });
const lines = buffer.split ('\n' );
buffer = lines.pop () || '' ;
for (const line of lines) {
if (line.trim ()) {
events.push (JSON .parse (line));
}
}
}
expect (events).toHaveLength (4 );
expect (events[0 ].event ).toBe ('start' );
expect (events[3 ].event ).toBe ('end' );
});
it ('should measure time-to-first-byte for chunked response' , async () => {
const startTime = performance.now ();
const response = await fetch (`${baseUrl} /ndjson` );
const reader = response.body !.getReader ();
const { value } = await reader.read ();
const ttfb = performance.now () - startTime;
expect (value).toBeTruthy ();
expect (ttfb).toBeLessThan (1000 );
await reader.cancel ();
});
});
gRPC Streaming Tests
import { describe, it, expect, beforeAll, afterAll } from 'vitest' ;
import * as grpc from '@grpc/grpc-js' ;
import * as protoLoader from '@grpc/proto-loader' ;
describe ('gRPC Server Streaming' , () => {
let client : any ;
let server : grpc.Server ;
beforeAll (async () => {
const packageDef = protoLoader.loadSync ('protos/stock.proto' );
const proto = grpc.loadPackageDefinition (packageDef) as any ;
server = new grpc.Server ();
server.addService (proto.stock .StockService .service , {
streamPrices : (call : any ) => {
const symbols = call.request .symbols ;
let tick = 0 ;
const interval = setInterval (() => {
tick++;
for (const symbol of symbols) {
call.write ({
symbol ,
price : 100 + Math .random () * 10 ,
tick,
timestamp : Date .now (),
});
}
if (tick >= 5 ) {
clearInterval (interval);
call.end ();
}
}, 100 );
call.on ('cancelled' , () => clearInterval (interval));
},
});
const port = await new Promise <number >((resolve, reject ) => {
server.bindAsync (
'0.0.0.0:0' ,
grpc.ServerCredentials .createInsecure (),
(err, port ) => {
if (err) reject (err);
else resolve (port);
}
);
});
server.start ();
client = new proto.stock .StockService (
`localhost:${port} ` ,
grpc.credentials .createInsecure ()
);
});
afterAll (() => {
server.forceShutdown ();
});
it ('should receive all price updates from server stream' , async () => {
const updates : any [] = [];
await new Promise <void >((resolve, reject ) => {
const call = client.streamPrices ({ symbols : ['AAPL' , 'GOOG' ] });
call.on ('data' , (update : any ) => {
updates.push (update);
});
call.on ('end' , () => resolve ());
call.on ('error' , (err : Error ) => reject (err));
});
expect (updates).toHaveLength (10 );
const applUpdates = updates.filter ((u ) => u.symbol === 'AAPL' );
const ticks = applUpdates.map ((u ) => u.tick );
expect (ticks).toEqual ([1 , 2 , 3 , 4 , 5 ]);
for (const update of updates) {
expect (update.price ).toBeGreaterThan (90 );
expect (update.price ).toBeLessThan (120 );
}
});
it ('should handle client cancellation of server stream' , async () => {
const updates : any [] = [];
await new Promise <void >((resolve ) => {
const call = client.streamPrices ({ symbols : ['AAPL' ] });
call.on ('data' , (update : any ) => {
updates.push (update);
if (updates.length >= 2 ) {
call.cancel ();
}
});
call.on ('error' , (err : any ) => {
if (err.code === grpc.status .CANCELLED ) {
resolve ();
}
});
});
expect (updates.length ).toBeGreaterThanOrEqual (2 );
expect (updates.length ).toBeLessThan (10 );
});
});
describe ('gRPC Bidirectional Streaming' , () => {
it ('should support bidirectional message exchange' , async () => {
const received : any [] = [];
await new Promise <void >((resolve, reject ) => {
const call = client.tradeChat ();
call.on ('data' , (msg : any ) => {
received.push (msg);
});
call.on ('end' , () => resolve ());
call.on ('error' , (err : Error ) => reject (err));
call.write ({ user : 'trader1' , message : 'Buy AAPL' });
call.write ({ user : 'trader1' , message : 'Sell GOOG' });
call.end ();
});
expect (received.length ).toBeGreaterThan (0 );
for (const msg of received) {
expect (msg).toHaveProperty ('user' );
expect (msg).toHaveProperty ('message' );
}
});
});
AI/LLM Streaming Response Testing
import { describe, it, expect, beforeAll, afterAll } from 'vitest' ;
import { createServer, Server } from 'http' ;
describe ('LLM Streaming Response' , () => {
let server : Server ;
let baseUrl : string ;
beforeAll (async () => {
server = createServer ((req, res ) => {
if (req.url === '/v1/chat/completions' && req.method === 'POST' ) {
res.writeHead (200 , {
'Content-Type' : 'text/event-stream' ,
'Cache-Control' : 'no-cache' ,
});
const tokens = ['Hello' , ',' , ' how' , ' can' , ' I' , ' help' , ' you' , '?' ];
let index = 0 ;
const sendToken = ( ) => {
if (index < tokens.length ) {
const chunk = {
id : 'chatcmpl-abc123' ,
object : 'chat.completion.chunk' ,
created : Math .floor (Date .now () / 1000 ),
model : 'gpt-4' ,
choices : [
{
index : 0 ,
delta : { content : tokens[index] },
finish_reason : null ,
},
],
};
res.write (`data: ${JSON .stringify(chunk)} \n\n` );
index++;
setTimeout (sendToken, 30 );
} else {
const finalChunk = {
id : 'chatcmpl-abc123' ,
object : 'chat.completion.chunk' ,
created : Math .floor (Date .now () / 1000 ),
model : 'gpt-4' ,
choices : [
{
index : 0 ,
delta : {},
finish_reason : 'stop' ,
},
],
};
res.write (`data: ${JSON .stringify(finalChunk)} \n\n` );
res.write ('data: [DONE]\n\n' );
res.end ();
}
};
sendToken ();
}
});
await new Promise <void >((resolve ) => {
server.listen (0 , () => {
const addr = server.address ();
baseUrl = `http://localhost:${typeof addr === 'object' ? addr!.port : 0 } ` ;
resolve ();
});
});
});
afterAll (() => server.close ());
it ('should collect all tokens from streaming response' , async () => {
const response = await fetch (`${baseUrl} /v1/chat/completions` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' },
body : JSON .stringify ({
model : 'gpt-4' ,
messages : [{ role : 'user' , content : 'Hello' }],
stream : true ,
}),
});
const reader = response.body !.getReader ();
const decoder = new TextDecoder ();
const tokens : string [] = [];
let finishReason : string | null = null ;
let receivedDone = false ;
let buffer = '' ;
while (true ) {
const { done, value } = await reader.read ();
if (done) break ;
buffer += decoder.decode (value, { stream : true });
const lines = buffer.split ('\n' );
buffer = lines.pop () || '' ;
for (const line of lines) {
if (line.startsWith ('data: ' )) {
const data = line.slice (6 ).trim ();
if (data === '[DONE]' ) {
receivedDone = true ;
continue ;
}
const parsed = JSON .parse (data);
const delta = parsed.choices [0 ].delta ;
if (delta.content ) {
tokens.push (delta.content );
}
if (parsed.choices [0 ].finish_reason ) {
finishReason = parsed.choices [0 ].finish_reason ;
}
}
}
}
const fullText = tokens.join ('' );
expect (fullText).toBe ('Hello, how can I help you?' );
expect (finishReason).toBe ('stop' );
expect (receivedDone).toBe (true );
expect (tokens).toHaveLength (8 );
});
it ('should measure token latency' , async () => {
const response = await fetch (`${baseUrl} /v1/chat/completions` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' },
body : JSON .stringify ({
model : 'gpt-4' ,
messages : [{ role : 'user' , content : 'Hello' }],
stream : true ,
}),
});
const reader = response.body !.getReader ();
const decoder = new TextDecoder ();
const tokenTimestamps : number [] = [];
let buffer = '' ;
while (true ) {
const { done, value } = await reader.read ();
if (done) break ;
buffer += decoder.decode (value, { stream : true });
const lines = buffer.split ('\n' );
buffer = lines.pop () || '' ;
for (const line of lines) {
if (line.startsWith ('data: ' ) && line.slice (6 ).trim () !== '[DONE]' ) {
const parsed = JSON .parse (line.slice (6 ));
if (parsed.choices [0 ].delta .content ) {
tokenTimestamps.push (performance.now ());
}
}
}
}
const latencies : number [] = [];
for (let i = 1 ; i < tokenTimestamps.length ; i++) {
latencies.push (tokenTimestamps[i] - tokenTimestamps[i - 1 ]);
}
const avgLatency = latencies.reduce ((a, b ) => a + b, 0 ) / latencies.length ;
const maxLatency = Math .max (...latencies);
expect (avgLatency).toBeLessThan (200 );
expect (maxLatency).toBeLessThan (500 );
});
it ('should handle abort during streaming' , async () => {
const controller = new AbortController ();
const tokensReceived : string [] = [];
const response = await fetch (`${baseUrl} /v1/chat/completions` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' },
body : JSON .stringify ({
model : 'gpt-4' ,
messages : [{ role : 'user' , content : 'Hello' }],
stream : true ,
}),
signal : controller.signal ,
});
const reader = response.body !.getReader ();
const decoder = new TextDecoder ();
try {
let buffer = '' ;
while (true ) {
const { done, value } = await reader.read ();
if (done) break ;
buffer += decoder.decode (value, { stream : true });
const lines = buffer.split ('\n' );
buffer = lines.pop () || '' ;
for (const line of lines) {
if (line.startsWith ('data: ' ) && line.slice (6 ).trim () !== '[DONE]' ) {
const parsed = JSON .parse (line.slice (6 ));
if (parsed.choices [0 ].delta .content ) {
tokensReceived.push (parsed.choices [0 ].delta .content );
if (tokensReceived.length >= 3 ) {
controller.abort ();
}
}
}
}
}
} catch (err : any ) {
expect (err.name ).toBe ('AbortError' );
}
expect (tokensReceived.length ).toBeGreaterThanOrEqual (3 );
expect (tokensReceived.length ).toBeLessThan (8 );
});
});
Backpressure and Flow Control Testing
import { describe, it, expect } from 'vitest' ;
import { createServer, Server } from 'http' ;
import { Readable , Transform } from 'stream' ;
describe ('Backpressure Testing' , () => {
it ('should handle slow consumer without losing data' , async () => {
const totalEvents = 100 ;
const consumerDelayMs = 10 ;
const server = createServer ((req, res ) => {
res.writeHead (200 , { 'Content-Type' : 'text/event-stream' });
for (let i = 0 ; i < totalEvents; i++) {
const canWrite = res.write (`data: ${JSON .stringify({ seq: i })} \n\n` );
if (!canWrite) {
res.once ('drain' , () => {});
}
}
res.end ();
});
const port = await new Promise <number >((resolve ) => {
server.listen (0 , () => {
const addr = server.address ();
resolve (typeof addr === 'object' ? addr!.port : 0 );
});
});
const response = await fetch (`http://localhost:${port} /events` );
const reader = response.body !.getReader ();
const decoder = new TextDecoder ();
const received : number [] = [];
let buffer = '' ;
while (true ) {
const { done, value } = await reader.read ();
if (done) break ;
await new Promise ((r ) => setTimeout (r, consumerDelayMs));
buffer += decoder.decode (value, { stream : true });
const lines = buffer.split ('\n' );
buffer = lines.pop () || '' ;
for (const line of lines) {
if (line.startsWith ('data: ' )) {
const data = JSON .parse (line.slice (6 ));
received.push (data.seq );
}
}
}
server.close ();
expect (received).toHaveLength (totalEvents);
expect (received).toEqual (Array .from ({ length : totalEvents }, (_, i ) => i));
});
it ('should detect memory leaks in long-running streams' , async () => {
const server = createServer ((req, res ) => {
res.writeHead (200 , { 'Content-Type' : 'text/event-stream' });
let count = 0 ;
const interval = setInterval (() => {
count++;
const largePayload = 'x' .repeat (1024 );
res.write (`data: ${JSON .stringify({ count, payload: largePayload })} \n\n` );
if (count >= 1000 ) {
clearInterval (interval);
res.end ();
}
}, 1 );
req.on ('close' , () => clearInterval (interval));
});
const port = await new Promise <number >((resolve ) => {
server.listen (0 , () => {
const addr = server.address ();
resolve (typeof addr === 'object' ? addr!.port : 0 );
});
});
const memBefore = process.memoryUsage ().heapUsed ;
const response = await fetch (`http://localhost:${port} /events` );
const reader = response.body !.getReader ();
let eventCount = 0 ;
while (true ) {
const { done } = await reader.read ();
if (done) break ;
eventCount++;
}
const memAfter = process.memoryUsage ().heapUsed ;
const memGrowthMB = (memAfter - memBefore) / (1024 * 1024 );
server.close ();
expect (eventCount).toBeGreaterThan (0 );
expect (memGrowthMB).toBeLessThan (50 );
});
});
Connection Resilience Testing
export interface StreamCollectorOptions {
timeoutMs ?: number ;
maxEvents ?: number ;
onEvent ?: (event : any ) => void ;
}
export async function collectSSEEvents (
url : string ,
options : StreamCollectorOptions = {}
): Promise <{ events : any []; errors : Error []; reconnections : number }> {
const { timeoutMs = 10000 , maxEvents = Infinity } = options;
const events : any [] = [];
const errors : Error [] = [];
let reconnections = 0 ;
return new Promise ((resolve ) => {
const timeout = setTimeout (() => {
es.close ();
resolve ({ events, errors, reconnections });
}, timeoutMs);
const es = new EventSource (url);
es.onopen = () => {
if (events.length > 0 ) reconnections++;
};
es.onmessage = (event ) => {
const data = JSON .parse (event.data );
events.push (data);
options.onEvent ?.(data);
if (events.length >= maxEvents) {
clearTimeout (timeout);
es.close ();
resolve ({ events, errors, reconnections });
}
};
es.onerror = (err ) => {
errors.push (new Error ('SSE connection error' ));
};
});
}
Streaming Performance Testing
"""
Load test for streaming endpoints using asyncio.
Simulates multiple concurrent SSE consumers.
"""
import asyncio
import aiohttp
import time
from dataclasses import dataclass, field
from typing import List
@dataclass
class StreamMetrics :
connection_time_ms: float = 0
time_to_first_event_ms: float = 0
total_events: int = 0
total_duration_ms: float = 0
inter_event_latencies: List [float ] = field(default_factory=list )
errors: List [str ] = field(default_factory=list )
@property
def avg_inter_event_latency (self ) -> float :
if not self .inter_event_latencies:
return 0
return sum (self .inter_event_latencies) / len (self .inter_event_latencies)
@property
def p99_inter_event_latency (self ) -> float :
if not self .inter_event_latencies:
return 0
sorted_latencies = sorted (self .inter_event_latencies)
idx = int (len (sorted_latencies) * 0.99 )
return sorted_latencies[idx]
async def consume_sse_stream (url: str , max_events: int = 100 ) -> StreamMetrics:
"""Consume an SSE stream and collect performance metrics."""
metrics = StreamMetrics()
start = time.monotonic()
try :
async with aiohttp.ClientSession() as session:
connect_start = time.monotonic()
async with session.get(url) as response:
metrics.connection_time_ms = (time.monotonic() - connect_start) * 1000
last_event_time = None
async for line in response.content:
decoded = line.decode('utf-8' ).strip()
if decoded.startswith('data: ' ):
now = time.monotonic()
if metrics.total_events == 0 :
metrics.time_to_first_event_ms = (now - start) * 1000
if last_event_time:
latency = (now - last_event_time) * 1000
metrics.inter_event_latencies.append(latency)
last_event_time = now
metrics.total_events += 1
if metrics.total_events >= max_events:
break
except Exception as e:
metrics.errors.append(str (e))
metrics.total_duration_ms = (time.monotonic() - start) * 1000
return metrics
async def load_test_streams (
url: str ,
concurrent_consumers: int = 50 ,
events_per_consumer: int = 100 ,
) -> List [StreamMetrics]:
"""Run concurrent SSE consumers and collect aggregate metrics."""
tasks = [
consume_sse_stream(url, events_per_consumer)
for _ in range (concurrent_consumers)
]
return await asyncio.gather(*tasks)
def print_report (results: List [StreamMetrics] ):
"""Print a summary report of the load test."""
successful = [r for r in results if not r.errors]
failed = [r for r in results if r.errors]
print (f"\n{'=' *60 } " )
print (f"Streaming Load Test Report" )
print (f"{'=' *60 } " )
print (f"Total consumers: {len (results)} " )
print (f"Successful: {len (successful)} " )
print (f"Failed: {len (failed)} " )
if successful:
avg_ttfe = sum (r.time_to_first_event_ms for r in successful) / len (successful)
avg_conn = sum (r.connection_time_ms for r in successful) / len (successful)
all_latencies = [l for r in successful for l in r.inter_event_latencies]
all_latencies.sort()
print (f"\nAvg connection time: {avg_conn:.1 f} ms" )
print (f"Avg time to first event: {avg_ttfe:.1 f} ms" )
if all_latencies:
p50 = all_latencies[len (all_latencies) // 2 ]
p95 = all_latencies[int (len (all_latencies) * 0.95 )]
p99 = all_latencies[int (len (all_latencies) * 0.99 )]
print (f"Inter-event latency p50: {p50:.1 f} ms" )
print (f"Inter-event latency p95: {p95:.1 f} ms" )
print (f"Inter-event latency p99: {p99:.1 f} ms" )
if __name__ == '__main__' :
import sys
url = sys.argv[1 ] if len (sys.argv) > 1 else 'http://localhost:3000/events'
results = asyncio.run(load_test_streams(url, concurrent_consumers=50 ))
print_report(results)
Best Practices
Always set timeouts on stream consumers -- A test that waits forever for a stream event blocks the entire suite.
Use AbortController for fetch-based streams -- Clean cancellation prevents resource leaks in tests.
Validate intermediate state, not just final state -- Streaming is about the journey; assert on each chunk.
Buffer partial data correctly -- Chunks can split across read boundaries; always use a line buffer.
Test empty streams -- A stream that opens and immediately closes should not crash the consumer.
Measure time-to-first-byte separately -- TTFB is the most critical streaming performance metric.
Test with realistic payload sizes -- Small test payloads may miss backpressure and buffering issues.
Close streams in afterEach/afterAll -- Leaked connections cause flaky tests and port exhaustion.
Test the [DONE] signal -- For LLM streams, verify the termination protocol is handled correctly.
Use mock servers, not production endpoints -- Tests must be deterministic; real streaming services are not.
Anti-Patterns to Avoid
Collecting entire stream before asserting -- This defeats the purpose of testing streaming; validate incrementally.
Using setTimeout as synchronization -- Use event-driven assertions (on data, on end) instead of arbitrary delays.
Ignoring partial reads -- A single reader.read() call may not return a complete event; always buffer.
Not testing connection drops -- Real networks fail; simulate disconnections and verify recovery.
Hardcoding port numbers -- Use port 0 and let the OS assign a free port to avoid conflicts.
Skipping error event testing -- The SSE onerror and gRPC on('error') handlers need test coverage.
Testing only happy path timing -- Measure latency under load, not just with a single consumer.
Forgetting to drain the stream -- If a test does not consume the full stream, it may leave the server hanging.
Not validating Content-Type headers -- text/event-stream for SSE is required; wrong headers cause silent failures.
Sharing server instances across parallel tests -- Each test should have its own server to avoid interference.
Running Tests
npx vitest run tests/sse/ tests/grpc/ tests/chunked/ tests/llm/
npx vitest run tests/sse/
npx vitest run tests/grpc/
npx vitest run tests/llm/
npx vitest run tests/ --reporter=verbose
python3 tests/performance/stream_load_test.py http://localhost:3000/events
npx vitest run tests/ --coverage
npx vitest watch tests/sse/
npx vitest run tests/llm/llm-stream.test.ts --reporter=verbose