{"id":17193703,"url":"https://github.com/andrei-markeev/pascal-web-server","last_synced_at":"2026-01-05T14:47:39.856Z","repository":{"id":209348596,"uuid":"723800760","full_name":"andrei-markeev/pascal-web-server","owner":"andrei-markeev","description":"Fast and lightweight http web server in Pascal, tailored for NGINX and MongoDB","archived":false,"fork":false,"pushed_at":"2023-11-28T21:27:18.000Z","size":34,"stargazers_count":0,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-01-30T06:41:34.733Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Pascal","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/andrei-markeev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-11-26T19:50:44.000Z","updated_at":"2023-11-28T19:04:51.000Z","dependencies_parsed_at":null,"dependency_job_id":"b6e0f18c-c81a-459a-88e2-b1c103e2da18","html_url":"https://github.com/andrei-markeev/pascal-web-server","commit_stats":null,"previous_names":["andrei-markeev/pascalwebserver"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andrei-markeev%2Fpascal-web-server","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andrei-markeev%2Fpascal-web-server/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andrei-markeev%2Fpascal-web-server/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andrei-markeev%2Fpascal-web-server/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/andrei-markeev","download_url":"https://codeload.github.com/andrei-markeev/pascal-web-server/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245415162,"owners_count":20611497,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-15T01:44:46.793Z","updated_at":"2026-01-05T14:47:39.799Z","avatar_url":"https://github.com/andrei-markeev.png","language":"Pascal","funding_links":[],"categories":[],"sub_categories":[],"readme":"## Pascal Web Server\n\nInvestigation into creating a fast modern web server in Pascal.\n\nThe assumption is that this server will run behind a reverse proxy (e.g. NGINX) and so it doesn't need to deal with things like TLS, authentication, rate limiting, serving static assets, etc. It is tailored to parsing requests forwarded from the proxy, working with MongoDB database, and returning responses in JSON or HTML format.\n\nThe focus of the work is to ensure that the server is fast, lightweight and uses minimum resources (CPU and RAM), but at the same time provides a relatively high level interface for handling requests, making DB calls and working with business logic.\n\nFeatures:\n- asynchronous sockets (e.g. epoll on Linux) via lNet library\n- bindings for MongoDB C Driver\n- thread pool for MongoDB tasks (dynamically expands / shrinks according to the load profile)\n\n### Getting started\n\n#### Database interface\n\nFirst, you need to create a database class that provides interface to your MongoDB database.\n\nFor example, TMyDatabase below contains two collections, Users and Orders:\n\n```pascal\nunit MyDb;\n\n{$mode objfpc}\n\ninterface\n\nuses\n    MongoDbPool, MongoDbCollection, DBSchema;\n\ntype\n\n    TMyDatabase = class\n    public type\n        TUserCollection = specialize TMongoDbCollection\u003cTUser\u003e;\n        TOrderCollection = specialize TMongoDbCollection\u003cTOrder\u003e;\n    private\n        pool: TMongoDbPool;\n        client: pointer;\n    public\n        Users: TUserCollection;\n        Orders: TOrderCollection;\n        constructor Create(mongoDbPool: TMongoDbPool);\n        destructor Destroy; override;\n    end;\n\nimplementation\n\nconstructor TMyDatabase.Create(mongoDbPool: TMongoDbPool);\nbegin\n    pool := mongoDbPool;\n    client := pool.GetClientFromThePool;\n\n    Users := TUserCollection.Create(client, 'mydb', 'users');\n    Orders := TOrderCollection.Create(client, 'mydb', 'orders');\nend;\n\ndestructor TMyDatabase.Destroy;\nbegin\n    Users.Free;\n    Orders.Free;\n\n    pool.ReturnClientToThePool(client);\n    inherited;\nend;\n\nend.\n```\n\nIn this example, `DBSchema` is another unit that contains `TOrder` and `TUser` classes, which could look something like this:\n\n```pascal\ntype\n    TUser = class\n        _id: string;\n        name: string;\n        // ... other fields\n\n        constructor Create;\n        constructor Create(doc: pbson_t);\n        destructor Destroy; override;\n    end;\n```\n\nImportantly, each DB model class should contain a constructor that accepts `pbson_t` and parses the object from BSON.\n\nBSON parsing is done with `libbson`, so you can more or less use examples from [official libbson documentation](https://mongoc.org/libbson/current/parsing.html).\n\nExample parsing from BSON: https://github.com/andrei-markeev/pascal-web-server/blob/main/DBSchema/OfficeLocation.pas#L55\n\n#### Web server\n\nRunning the server is as simple as:\n\n```pascal\n\nvar\n    server: TPascalWebServer;\n\nbegin\n    server := TPascalWebServer.Create(@ProcessRequest);\n    server.Listen(3000);\n    server.Free;\nend.\n\n```\n\nFor handling requests, you need to provide `ProcessRequest` method which would route requests to the correct handlers.\n\nFor example:\n\n```pascal\n    procedure ProcessRequest(request: TRequest; socket: TLSocket);\n    var\n        task: TTask;\n    begin\n        case request.url of\n            '/users':\n            begin\n                task := TUsersPageTask.Create(pool, socket);\n                server.EnqueueTask(task);\n            end;\n            '/orders':\n            begin\n                task := TOrdersPageTask.Create(pool, socket);\n                server.EnqueueTask(task);\n            end;\n        else\n            socket.SendMessage('HTTP/1.1 404' + CRLF + 'Content-length: 0' + CRLF + CRLF);\n        end;\n    end;\n```\n\nRequest object represent a parsed request:\n\n```pascal\n    TRequest = record\n        method: (methodUnknown, methodGET, methodPOST, methodPUT, methodPATCH, methodDELETE);\n        url: string;\n        headers: array of THeader;\n        body: string;\n    end;\n```\n\nIf you need to work with MongoDB, use tasks. Otherwise, you can just create a response and send it back to the socket.\n\n#### Tasks\n\nTasks should be inherited from `TTask`.\n\nEach task is split into two parts:\n- first part is called `Execute` and runs in one of worker threads (each worker thread has it's own connection to MongoDB), this is where you use `TMyDatabase`\n- second part is called `Finalize` and runs in the main thread, this is where you send response back to the client\n\nFor example:\n\n```pascal\nprocedure TUserProfilePageTask.Execute;\nvar\n    db: TMyDatabase;\n    query: pbson_t;\nbegin\n    db := TMyDatabase.Create(pool);\n    query := bson_new;\n    bson_append_utf8(query, '_id', length('_id'), userId, length(userId));\n    user := db.Users.findOne(query);\n    bson_destroy(query);\n    db.Free;\nend;\n\nprocedure TUserProfilePageTask.Finalize;\nvar\n    i: integer;\n    html: string;\n    body: string;\nbegin\n    html := '\u003c!DOCTYPE html\u003e\u003chtml\u003e\u003ch1\u003eYour profile\u003c/h1\u003e\u003cp\u003e\u003cul\u003e'\n        + '\u003cli\u003e\u003cstrong\u003eName:\u003c/strong\u003e ' + user.name + '\u003c/li\u003e'\n        + '\u003cli\u003e\u003cstrong\u003eEmail:\u003c/strong\u003e ' + user.email + '\u003c/li\u003e'\n        + '\u003c/ul\u003e';\n\n    user.Free;\n\n    body := 'HTTP/1.1 200' + CRLF\n        + 'Content-type: text/html' + CRLF\n        + 'Content-length: ' + IntToStr(Length(html)) + CRLF\n        + CRLF\n        + html;\n\n    if (status \u003c\u003e tsCancelled) and (socket.ConnectionStatus = scConnected) then\n        socket.SendMessage(body);\n\nend;\n```\n\nSee full example here: https://github.com/andrei-markeev/pascal-web-server/blob/main/Tasks/LocationAsHtml.pas\n\n**Note**: If you don't need MongoDB, don't use `EnqueueTask`, simply send the data to the socket right from `ProcessRequest`.\nThe whole tasks system was devised because [MongoDB C Driver doesn't expose async API](https://www.mongodb.com/community/forums/t/why-not-supply-async-api-in-mongo-c-driver/16260), so we have to deal with thread pool and the corresponding complexity.\n\n\n### Benchmarks\n\nAll tests are run on same machine, in WSL.\n\nI know that it is not optimal, but I think it still gives an idea of the relative performance.\n\nAlso, just a note, throughput can vary a bit between the runs. Probably depends on other tasks that are running on same machine. I'm trying to present average performing runs.\n\n#### Hello world\n\nServer should return `\u003c!DOCTYPE html\u003e\u003chtml\u003e\u003ch1\u003eHello worlde!\u003c/h1\u003e\u003c/html\u003e` with correct `Content-Type` header.\n\n**server_ltcp**:\n\nWith 10 concurrent connections, uses 9.4Mb RAM, throughput 65.9k rps.\n\n```\nRunning 30s test @ http://localhost:3000/hello\n  2 threads and 10 connections\n  Thread Stats   Avg      Stdev     Max   +/- Stdev\n    Latency   187.22us  472.16us  23.28ms   98.70%\n    Req/Sec    33.19k     8.54k   97.85k    68.39%\n  1984403 requests in 30.10s, 210.06MB read\nRequests/sec:  65927.60\nTransfer/sec:      6.98MB\n```\n\n**server_nodehttp**:\n\nWith 10 concurrent connections, uses 76.1Mb RAM, throughput 13.5k rps.\n\n```\nRunning 30s test @ http://localhost:3000/hello\n  2 threads and 10 connections\n  Thread Stats   Avg      Stdev     Max   +/- Stdev\n    Latency   746.38us  241.36us  12.07ms   93.28%\n    Req/Sec     6.79k   456.07     7.20k    94.19%\n  406458 requests in 30.10s, 84.12MB read\nRequests/sec:  13503.67\nTransfer/sec:      2.79MB\n```\n\n#### JSON serialization\n\nServer should serialize an object to JSON format and return it with correct `Content-Type` header.\nJSON strings should be properly escaped.\n\nThe object has the following structure (taken from a real production application):\n\n```ts\ninterface OfficeLocation {\n    _id: string,\n    tenantId: string,\n    name: string,\n    address: string,\n    latitude?: number,\n    longitude?: number,\n    email?: string,\n    phone?: string,\n    sendICalNotifications: boolean,\n    notificationEmail?: string,\n    cateringOrderEmail?: string,\n    options: {\n        availableFromHour?: number,\n        availableUntilHour?: number,\n        altSchedule?: ('Mon' | 'Tue' | 'Wed' | 'Thu' | 'Fri' | 'Sat' | 'Sun')[],\n        altAvailableFromHour?: number,\n        altAvailableUntilHour?: number\n    }\n}\n```\n\n**server_ltcp**:\n\nWith 10 concurrent connections, uses 9.5Mb RAM, throughput 34.5k rps.\n\n```\nRunning 30s test @ http://localhost:3000/json\n  2 threads and 10 connections\n  Thread Stats   Avg      Stdev     Max   +/- Stdev\n    Latency   417.47us    1.10ms  24.53ms   97.78%\n    Req/Sec    17.39k     4.31k   33.83k    78.83%\n  1038414 requests in 30.03s, 724.91MB read\nRequests/sec:  34579.20\nTransfer/sec:     24.14MB\n```\n\nWith 150 concurrent connections, uses 9.5Mb RAM, throughput increases up to 48.2k rps.\n\n```\nRunning 10s test @ http://localhost:3000/json\n  2 threads and 150 connections\n  Thread Stats   Avg      Stdev     Max   +/- Stdev\n    Latency    10.10ms   88.16ms   1.68s    98.58%\n    Req/Sec    24.26k     8.44k   40.53k    64.50%\n  482655 requests in 10.00s, 336.94MB read\n  Socket errors: connect 0, read 0, write 0, timeout 12\nRequests/sec:  48254.72\nTransfer/sec:     33.69MB\n```\n\n**server_nodehttp**:\n\nWith 10 concurrent connections, uses 77.9Mb RAM, throughput 13.1k rps.\n\n```\nRunning 30s test @ http://localhost:3000/json\n  2 threads and 10 connections\n  Thread Stats   Avg      Stdev     Max   +/- Stdev\n    Latency     0.86ms    1.10ms  34.09ms   98.05%\n    Req/Sec     6.63k     0.96k    8.94k    92.67%\n  395709 requests in 30.03s, 315.49MB read\nRequests/sec:  13176.90\nTransfer/sec:     10.51MB\n```\n\nWith 150 concurrent connections, uses 82.5Mb RAM, throughput *decreases* down to 11.3k rps.\n\n```\nRunning 10s test @ http://localhost:3000/json\n  2 threads and 150 connections\n  Thread Stats   Avg      Stdev     Max   +/- Stdev\n    Latency    13.18ms    1.99ms  44.04ms   94.42%\n    Req/Sec     5.73k   578.32     6.16k    94.00%\n  114003 requests in 10.01s, 90.89MB read\nRequests/sec:  11391.06\nTransfer/sec:      9.08MB\n```\n\n### Fetch JSON from MongoDB\n\nServer should fetch data from MongoDB and serialize it.\n\n**server_ltcp**:\n\nWith 150 concurrent connections, uses 37.8Mb RAM, throughput 1.7k rps.\n\n```\nRunning 10s test @ http://localhost:3000/mongo\n  2 threads and 150 connections\n  Thread Stats   Avg      Stdev     Max   +/- Stdev\n    Latency    90.81ms  159.85ms   1.89s    94.74%\n    Req/Sec     0.90k   404.41     1.59k    61.42%\n  17682 requests in 10.03s, 6.27MB read\n  Socket errors: connect 0, read 0, write 0, timeout 21\nRequests/sec:   1763.70\nTransfer/sec:    640.72KB\n```\n\n**server_nodehttp**:\n\nWith 150 concurrent connections, uses 113Mb RAM, throughput 1.1k rps.\n\n```\nRunning 10s test @ http://localhost:3000/mongo\n  2 threads and 150 connections\n  Thread Stats   Avg      Stdev     Max   +/- Stdev\n    Latency   144.79ms  105.96ms 979.14ms   91.06%\n    Req/Sec   580.20    223.98     0.86k    76.38%\n  11515 requests in 10.02s, 7.73MB read\nRequests/sec:   1149.23\nTransfer/sec:    790.09KB\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandrei-markeev%2Fpascal-web-server","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fandrei-markeev%2Fpascal-web-server","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandrei-markeev%2Fpascal-web-server/lists"}