diff --git a/README.md b/README.md
index d027085..7d511e9 100644
--- a/README.md
+++ b/README.md
@@ -48,6 +48,10 @@ Checkout my [Notion](https://mohitjain.notion.site/Coding-Challenges-af9b8197a43
27. [Write Your Own Rate Limiter](src/27/)
28. [Write Your Own NTP Client](src/28/)
+Don't ask me about this GAP. This ain't an interview!
+
+41. [Write Your Own HTTP(S) Load Tester](src/41/)
+
## Installation
The following command will build all the .ts files present in `src` folder into a new `build` folder.
diff --git a/src/41/README.md b/src/41/README.md
new file mode 100644
index 0000000..aad2b10
--- /dev/null
+++ b/src/41/README.md
@@ -0,0 +1,44 @@
+# Challenge 41 - Write Your Own HTTP(S) Load Tester
+
+This challenge corresponds to the 41st part of the Coding Challenges series by John Crickett https://codingchallenges.fyi/challenges/challenge-load-tester.
+
+## Description
+
+The objective of the challenge is to build a HTTP(s) Load Tester that can query a given address and return some important stats such as Total Request Time, Time to First Byte, Time to Last Byte.
+
+## Usage
+
+You can use the `ts-node` tool to run the command line version of the NTP Client as follows:
+
+```bash
+npx ts-node -u [-n ] [-c ]
+```
+
+### Options
+
+- `-u `: The URL on which load testing needs to be performed.
+
+- `-n `: The number of requests sent to the server. This are sent in series. Default = 10.
+
+- `-c `: The number of concurrent requests to send. Default = 1.
+
+### Examples
+
+```bash
+# Load test https://google.com with 10 requests and 10 concurrency
+npx ts-node -u https://google.com -n 10 -c 10
+```
+
+### Description
+
+- [customer_request.ts](custom_request.ts): A helper function which calculates some stats while doing a network GET request. The code is inspired from https://gabrieleromanato.name/nodejs-get-the-time-to-first-byte-ttfb-of-a-website.
+
+- [load_tester.ts](load_tester.ts): The main load tester implementation.
+
+## Run tests
+
+To run the tests for the Load Tester, go to the root directory of this repository and run the following command:
+
+```bash
+npm test src/41/
+```
diff --git a/src/41/__tests__/custom_request.test.ts b/src/41/__tests__/custom_request.test.ts
new file mode 100644
index 0000000..8f51dad
--- /dev/null
+++ b/src/41/__tests__/custom_request.test.ts
@@ -0,0 +1,31 @@
+import { customRequest } from '../custom_request';
+
+describe('Testing custom request', () => {
+ it('should handle redirects', async () => {
+ const url = 'https://google.com';
+ const stats = await customRequest(url);
+ expect(stats.statusCode).toBe(200);
+ });
+
+ it('should handle http requests', async () => {
+ const url = 'http://google.com';
+ const stats = await customRequest(url);
+ expect(stats.statusCode).toBe(200);
+ });
+
+ it('should handle https requests', async () => {
+ const url = 'https://www.google.com';
+ const stats = await customRequest(url);
+ expect(stats.statusCode).toBe(200);
+ });
+
+ it('should raise error on invalid URL', (done) => {
+ const url = 'http://123213213123123.com';
+ customRequest(url).catch(() => done());
+ });
+
+ it('should raise error on invalid protocol', (done) => {
+ const url = '123213213123123.com';
+ customRequest(url).catch(() => done());
+ });
+});
diff --git a/src/41/custom_request.ts b/src/41/custom_request.ts
new file mode 100644
index 0000000..b370b88
--- /dev/null
+++ b/src/41/custom_request.ts
@@ -0,0 +1,101 @@
+import https from 'https';
+import http from 'http';
+
+export type Stats = {
+ body: string;
+ statusCode: number;
+ trtMs: number;
+ ttfbMs: number;
+ ttlbMs: number;
+};
+
+export function customRequest(url: string): Promise {
+ const protocol = url.startsWith('https') ? https : http;
+
+ const stats: Stats = {
+ body: '',
+ statusCode: 0,
+ trtMs: 0,
+ ttfbMs: 0,
+ ttlbMs: 0
+ };
+
+ const processTimes = {
+ dnsLookup: BigInt(0),
+ tcpConnection: BigInt(0),
+ tlsHandshake: BigInt(0),
+ responseBodyStart: BigInt(0),
+ responseBodyEnd: BigInt(0)
+ };
+
+ // Ensure connections are not reused
+ const agent = new protocol.Agent({
+ keepAlive: false
+ });
+
+ return new Promise((resolve, reject) => {
+ // Close the connection after the request
+ const options = {
+ agent,
+ headers: {
+ Connection: 'close'
+ }
+ };
+
+ const req = protocol.get(url, options, (res) => {
+ stats.statusCode = res.statusCode ?? -1;
+
+ // Handle redirects
+ if (res.statusCode === 301 && res.headers.location) {
+ const redirectUrl = res.headers.location;
+ req.destroy();
+
+ customRequest(redirectUrl)
+ .then((redirectStats) => resolve(redirectStats))
+ .catch((err) => reject(err));
+ return;
+ }
+
+ res.once('data', () => {
+ processTimes.responseBodyStart = process.hrtime.bigint();
+ stats.ttfbMs =
+ Number(processTimes.responseBodyStart - processTimes.tlsHandshake) /
+ 1000_000;
+ });
+
+ res.on('data', (d) => {
+ stats.body += d;
+ });
+
+ res.on('end', () => {
+ processTimes.responseBodyEnd = process.hrtime.bigint();
+ stats.ttlbMs =
+ Number(processTimes.responseBodyEnd - processTimes.tlsHandshake) /
+ 1000_000;
+ stats.trtMs =
+ Number(processTimes.responseBodyEnd - processTimes.dnsLookup) /
+ 1000_000;
+ resolve(stats);
+ });
+ });
+
+ req.on('error', (error) => {
+ reject(error);
+ });
+
+ req.on('socket', (socket) => {
+ socket.on('lookup', () => {
+ processTimes.dnsLookup = process.hrtime.bigint();
+ });
+ socket.on('connect', () => {
+ processTimes.tcpConnection = process.hrtime.bigint();
+ });
+
+ socket.on('secureConnect', () => {
+ processTimes.tlsHandshake = process.hrtime.bigint();
+ });
+ });
+
+ req.end();
+ });
+}
diff --git a/src/41/index.ts b/src/41/index.ts
new file mode 100644
index 0000000..0cf05b1
--- /dev/null
+++ b/src/41/index.ts
@@ -0,0 +1,32 @@
+import { program } from 'commander';
+import { loadTester } from './load_tester';
+
+program.option('-u ', 'URL to load test on');
+program.option('-n ', 'Number of requests to make', '10');
+program.option(
+ '-c ',
+ 'Number of concurrent requests to make',
+ '1'
+);
+
+program.parse();
+
+const { u, n, c } = program.opts();
+
+try {
+ new URL(u);
+} catch (e) {
+ console.error((e as Error).message);
+ process.exit(1);
+}
+
+const numberOfRequests = parseInt(n);
+const concurrency = parseInt(c);
+const url = u.toString();
+
+async function main() {
+ const result = await loadTester({ numberOfRequests, concurrency, url });
+ console.log(result);
+}
+
+main();
diff --git a/src/41/load_tester.ts b/src/41/load_tester.ts
new file mode 100644
index 0000000..c659262
--- /dev/null
+++ b/src/41/load_tester.ts
@@ -0,0 +1,121 @@
+import { Stats, customRequest } from './custom_request';
+
+type LoadTesterParams = {
+ numberOfRequests: number;
+ concurrency: number;
+ url: string;
+};
+
+type TMinMaxMean = {
+ min: number;
+ max: number;
+ mean: number;
+};
+
+type TLoadTesterReturn = {
+ totalRequests: number;
+ successes: number;
+ failures: number;
+ totalTimeRequestMs: TMinMaxMean;
+ ttfbMs: TMinMaxMean;
+ ttlbMs: TMinMaxMean;
+ reqPerSec: number;
+};
+
+async function makeRequest(
+ url: string,
+ numberOfRequests: number = 10
+): Promise {
+ const stats: Stats[] = [];
+ for (let i = 0; i < numberOfRequests; i++) {
+ try {
+ stats.push(await customRequest(url));
+ } catch (_) {
+ stats.push({
+ body: '',
+ statusCode: 500,
+ trtMs: -1,
+ ttfbMs: -1,
+ ttlbMs: -1
+ });
+ }
+ }
+ return stats;
+}
+
+export async function loadTester({
+ numberOfRequests,
+ concurrency,
+ url
+}: LoadTesterParams): Promise {
+ const startTime = process.hrtime.bigint();
+
+ const promises = [];
+ for (let i = 0; i < concurrency; i++) {
+ promises.push(makeRequest(url, numberOfRequests));
+ }
+ const statLists = await Promise.all(promises);
+
+ const endTime = process.hrtime.bigint();
+
+ let successes = 0,
+ failures = 0;
+
+ // Max, Min and Mean for [ttfb, ttlb, trt]
+ const max = [-1, -1, -1],
+ min = [Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE],
+ mean = [0, 0, 0];
+
+ statLists.forEach((stats) => {
+ stats.forEach((stat) => {
+ if (stat.statusCode >= 200 && stat.statusCode <= 299) {
+ successes++;
+
+ // ttfbMs
+ if (stat.ttfbMs > max[0]) {
+ max[0] = stat.ttfbMs;
+ }
+ if (stat.ttfbMs < min[0]) {
+ min[0] = stat.ttfbMs;
+ }
+ mean[0] += stat.ttfbMs;
+
+ // ttlbMs
+ if (stat.ttlbMs > max[1]) {
+ max[1] = stat.ttlbMs;
+ }
+ if (stat.ttlbMs < min[1]) {
+ min[1] = stat.ttlbMs;
+ }
+ mean[1] += stat.ttlbMs;
+
+ // trtMs
+ if (stat.trtMs > max[2]) {
+ max[2] = stat.trtMs;
+ }
+ if (stat.trtMs < min[2]) {
+ min[2] = stat.trtMs;
+ }
+ mean[2] += stat.trtMs;
+ } else {
+ failures++;
+ }
+ });
+ });
+
+ mean[0] = Number((mean[0] / successes).toFixed(4));
+ mean[1] = Number((mean[1] / successes).toFixed(4));
+ mean[2] = Number((mean[2] / successes).toFixed(4));
+
+ return {
+ totalRequests: numberOfRequests * concurrency,
+ successes,
+ failures,
+ ttfbMs: { min: min[0], max: max[0], mean: mean[0] },
+ ttlbMs: { min: min[1], max: max[1], mean: mean[1] },
+ totalTimeRequestMs: { min: min[2], max: max[2], mean: mean[2] },
+ reqPerSec: Number(
+ (successes / (Number(endTime - startTime) / 1000_000_000)).toFixed(2)
+ )
+ };
+}