15 comments

  • russellthehippo9 hours ago
    Hey HN, I built this. Honker adds cross-process NOTIFY&#x2F;LISTEN to SQLite. You get push-style event delivery with single-digit millisecond latency without a damon&#x2F;broker, using your existing SQLite file. A lot of pretty high-traffic applications are just Framework+SQLite+Litestream on a VPS now, so I wanted to bring a sixer to the &quot;just use SQLite&quot; party.<p>SQLite doesn&#x27;t run a server like Postgres, so the trick is moving the polling source from interval queries on a SQLite connection to a lightweight stat(2) on the WAL file. Many small queries are efficient in SQLite (<a href="https:&#x2F;&#x2F;www.sqlite.org&#x2F;np1queryprob.html" rel="nofollow">https:&#x2F;&#x2F;www.sqlite.org&#x2F;np1queryprob.html</a>) so this isn&#x27;t really a huge upgrade, but the cross-language result is pretty interesting to me - this is language agnostic as all you do is listen to the WAL file and call SQLite functions.<p>On top of the store&#x2F;notify primitives, honker ships ephemeral pub&#x2F;sub (like pg_notify), durable work queues with retries and dead-letter (like pg-boss&#x2F;Oban), and event streams with per-consumer offsets. All three are rows in your app&#x27;s existing .db file and can commit atomically with your business write. This is cool because a rollback drops both.<p>This used to be called litenotify&#x2F;joblite but I bought honker.dev as a joke for my gf and I realized that every mq&#x2F;task&#x2F;worker have silly names: Oban, pg-boss, Huey, RabbitMQ, Celery, Sidekiq, etc. Thus a silly goose got its name.<p>Honker waddles the same path as these giants and honks into the same void.<p>Hopefully it&#x27;s either useful to you or is amusing. Standard alpha software warnings apply.
    • andersmurphy8 hours ago
      Is the main use case for this for languages that only have access to process based concurrency?<p>Struggling to see why you would otherwise need this in java&#x2F;go&#x2F;clojure&#x2F;C# your sqlite has a single writer, so you can notify all threads that care about inserts&#x2F;updates&#x2F;changes as your application manages the single writer (with a language level concurrent queue) so you know when it&#x27;s writing and what it has just written. So it always felt simpler&#x2F;cleaner to get notification semantics that way.<p>Still fun to see people abuse WAL in creative ways. Cool to see a notify mechanism that works for languages that only have process based concurrency python&#x2F;JS&#x2F;TS&#x2F;ruby. Nice work!
      • russellthehippo1 hour ago
        I actually hadn’t thought about it this way. The killer app I was imagining was 1ms reactivity without SQL polling and messaging atomic with business commits, plus “one db” and no daemon.<p>But this is actually a great main benefit as well.
      • infogulch8 hours ago
        He mentions Litestream, maybe this also works for litestream read-only replicas which may be in completely different locations?
        • russellthehippo1 hour ago
          Whoa I really hadn’t considered this. Do a litestream read replica, trigger across machines with S3 as the broker essentially. But you’re still stuck with the litestream sync interval. Maybe interesting for cross server notify?
          • infogulch1 hour ago
            I guess the idea is to have all writes go through a central server with local read replicas for improved read perf. The default litestream sync interval is 1s. I bet many use-cases would be satisfied with a few seconds delay for cross-region notifications.
            • russellthehippo5 minutes ago
              It&#x27;s good for pubsub but not for claim&#x2F;ack workflow unless you do If-None-Match CAS semantics on a separate filesystem which, actually, yeah that&#x27;s probably fine. Feels heavy on S3 ops. But! you do save on inter-AZ networking, the Warpstream hypothesis.
    • arowthway8 hours ago
      Nice, I had no idea that stat() every 1 ms is so affordable. Aparently it takes less than 1 μs per call on my hardware, so that&#x27;s less than 0.1% cpu time for polling.
      • WJW7 hours ago
        &quot;Syscalls are slow&quot; is only mostly true. They are slower than not having to cross the userspace &lt;-&gt; OS barrier at all, but they&#x27;re not &quot;slow&quot; like cross-ocean network calls can be. For example, non-VDSO syscalls in linux are about 250 nanoseconds (see for example <a href="https:&#x2F;&#x2F;arkanis.de&#x2F;weblog&#x2F;2017-01-05-measurements-of-system-call-performance-and-overhead&#x2F;" rel="nofollow">https:&#x2F;&#x2F;arkanis.de&#x2F;weblog&#x2F;2017-01-05-measurements-of-system-...</a>), VDSO syscalls are roughly 10x faster. Slower than userspace function calls for sure, but more than affordable outside the hottest of loops.
        • vlovich1236 hours ago
          Filesystem stuff tends to be slower than average syscalls because of all the locks and complicated traversals needed. If this is using stat instead of fstat then it’s also going through the VFS layer - repeated calls likely go through the cache fast path for path resolution but accessing the stat structure. There’s also hidden costs in that number like atomic accesses that need to acquire cache line locks that are going to cause hidden contention for other processes on the CPU + the cache dirtying from running kernel code and then subsequently having to repopulate it when leaving all of which adds contended L3&#x2F;RAM pressure.<p>In other words, there’s a lot of unmeasured performance degradation that’s a side effect of doing many syscalls above and beyond the CPU time to enter&#x2F;leave the kernel which itself has shrunk to be negligible. But there’s a reason high performance code is switching to io_uring to avoid that.
          • russellthehippo1 hour ago
            Oh cool, so using io uring plus pragma data version would actually beat stat on Linux holistically speaking? The stat choice was all about cross platform consistency over inotify speed. But syscalls overwhelm can be real.
        • slashdev7 hours ago
          That’s ignoring the other costs of syscalls like evicting your stuff from the CPU caches.<p>But I agree with the conclusion, system calls are still pretty fast compared to a lot of other things.
          • vlovich1236 hours ago
            Small correction on ambiguous wording - syscalls do not evict all your stuff from CPU caches. It just has to page in whatever is needed for kernel code&#x2F;data accessed by the call, but that’s no different from if it was done in process as a normal function call.
            • Polizeiposaune3 hours ago
              Depending on implementation details of your CPU and OS, the syscall path may need to flush various auxillary caches (like one or more TLBs) to prevent speculation attacks, which may put additional &quot;drag&quot; on your program after syscall return.
    • ncruces6 hours ago
      Probably missing something, why is `stat(2)` better than: `PRAGMA data_version`?<p><a href="https:&#x2F;&#x2F;sqlite.org&#x2F;pragma.html#pragma_data_version" rel="nofollow">https:&#x2F;&#x2F;sqlite.org&#x2F;pragma.html#pragma_data_version</a><p>Or for a C API that&#x27;s even better, `SQLITE_FCNTL_DATA_VERSION`:<p><a href="https:&#x2F;&#x2F;sqlite.org&#x2F;c3ref&#x2F;c_fcntl_begin_atomic_write.html#sqlitefcntldataversion" rel="nofollow">https:&#x2F;&#x2F;sqlite.org&#x2F;c3ref&#x2F;c_fcntl_begin_atomic_write.html#sql...</a>
      • infogulch5 hours ago
        Yeah the C API seems like a perfect fit for this use-case:<p>&gt; [SQLITE_FCNTL_DATA_VERSION] is the only mechanism to detect changes that happen either internally or externally and that are associated with a particular attached database.<p>Another user itt says the stat(2) approach takes less than 1 μs per call on their hardware.<p>I wonder how these approaches compare across compatibility &amp; performance metrics.
      • psadri6 hours ago
        For one it seems to be deprecated.
        • ncruces6 hours ago
          It&#x27;s not.
          • russellthehippo1 hour ago
            Yep, definitely still in use. Do yall above have an opinion if the pragma is better than the syscall? What are the trade offs there? Another comment thread mentioned this as well and pointed to io uring. I was thinking that dism spam is worse than syscall spam.
    • rich_sasha5 hours ago
      Pretty cool! I have a half baked version of something similar :)<p>Can you use it also as a lightweight Kafka - persistent message stream? With semantics like, replay all messages (historical+real time) from some timestamp for some topics?<p>As with pub&#x2F;sub, you can reproduce this with some polling etc but as you say, that&#x27;s not optimal.
      • russellthehippo1 hour ago
        Absolutely! That’s the durable pubsub angle for sure.
    • infogulch8 hours ago
      Neat idea!<p>Would it help if subscriber states were also stored? (read position, queue name, filters, etc) Then instead of waking all subscription threads to do their own N=1 SELECT when stat(2) changes, the polling thread could do Events INNER JOIN Subscribers and only wake the subscribers that match.
    • noveltyaccount8 hours ago
      This is really interesting. I&#x27;m building something on Postgresql with LISTEN&#x2F;NOTIFY and Postgraphile. I&#x27;d love to (in theory) be able to have a swappable backend and not be so tightly coupled to the database server.
    • hk13378 hours ago
      I love the name!
  • JoelJacobson5 hours ago
    Shameless plug: In the upcoming release of PostgreSQL 19, LISTEN&#x2F;NOTIFY has been optimized to scale much better with selective signaling, i.e. when lots of backends are listening on different channels, patch: <a href="https:&#x2F;&#x2F;git.postgresql.org&#x2F;gitweb&#x2F;?p=postgresql.git;a=commitdiff;h=282b1cde9" rel="nofollow">https:&#x2F;&#x2F;git.postgresql.org&#x2F;gitweb&#x2F;?p=postgresql.git;a=commit...</a>
  • Retr0id9 hours ago
    Couldn&#x27;t you use inotify (and&#x2F;or some cross-platform wrapper) to watch for WAL changes without polling?
    • russellthehippo1 hour ago
      Breaks cross-platform, specifically Macs swallow silently. stat just works
      • Retr0id1 hour ago
        I don&#x27;t believe this to be true.
        • russellthehippo1 hour ago
          See comment below - Darwin silently drops same-process notifs. I could change the behavior depending on same vs cross process and platform but I wanted to”just one thing to worry about”. Potentially a good optimization later. Would help reduce syscalls.
          • Retr0id1 hour ago
            I believe you are mistaken. If you are referring to the comment from ArielTM, that&#x27;s an LLM bot regurgitating your readme.
  • nzoschke6 hours ago
    Thanks for this!<p>I have a proliferation of small apps backed by SQLite. And most of these need a queue and scheduler.<p>I home rolled some stuff for it but was always pining for the elegance of the Postgres solutions.<p>Will give this a spin very soon
  • ArielTM9 hours ago
    kqueue&#x2F;FSEvents is tempting here, but Darwin drops same-process notifications. If you&#x27;ve got a publisher and listener in the same process the listener just never fires. Nasty thing to chase. stat polling looks gross but it&#x27;s the only thing that actually works everywhere.<p>What happens on WAL checkpoint? When the file shrinks back, does that trigger a wakeup, or does the poller filter size drops?
  • agentbonnybb1 hour ago
    This is the kind of skill-level tool I wish existed earlier — I hit the exact pain point running a daily-chronicle site off SQLite + a static deploy a week ago. Ended up with a crude polling loop because the alternatives all wanted me to install Postgres for a single notification semantic.<p>Question: any thoughts on what breaks first when a single process has 10k+ concurrent listeners? I&#x27;m curious whether the SQLite side can sustain what Postgres does cheaply.
    • russellthehippo1 hour ago
      10k listeners is a lot. Thundering herd issue at stat(). SQLite may not be your best choice at this scale.
  • robertlagrant4 hours ago
    If I&#x27;m using SQLAlchemy, can this integrate? It seems to want to make the db connection itself.
  • tuo-lei7 hours ago
    atomic commit with the business data is the selling point over separate IPC. external message passing always has the &#x27;notification sent but transaction rolled back&#x27; problem and that gets messy.<p>one thing i&#x27;m curious about: WAL checkpoint. when SQLite truncates WAL back to zero, does the stat() polling handle that correctly? feels like there&#x27;s a window where events could get lost.
    • russellthehippo1 hour ago
      The WAL file sticks around but gets truncated so that counts as an update. Though I don’t have tests for this. Good input, thanks I’ll make sure
  • PunchyHamster8 hours ago
    Wouldn&#x27;t processes on same machine be able to use different IPCs that don&#x27;t even touch file ? It&#x27;s neat but I have feeling in vast majority of cases just passing address to one of the IPC methods would be faster and then SQLite itself would only be needed for the durable parts.
    • blacklion7 hours ago
      This extension piggyback SQLite native transactions. For example, queueing data will be rolled back if transaction is rolled back due to some constrains violations.<p>It is possible to achieve with external IPC, but require a lot of very careful programming.
  • nodesocket5 hours ago
    Awesome. I’m currently using AWS SQS which invokes lambda functions for asynchronous tasks like email sends, but Honker seems like a great local replacement.<p>Any conflicts or issues when running Litestream as well?
    • russellthehippo1 hour ago
      Nope! The extension just functions as a shortcut for raw SQL. Litestream edits the wal file but only like a normal checkpoint. So not too bad. Although I haven’t tested it directly. Probably need to
  • yasirlatif5 hours ago
    [dead]
  • 0x1da498 hours ago
    [dead]
  • mastermanas12346 hours ago
    [dead]
  • faraway99113 hours ago
    [dead]
  • GangstaAgents9 hours ago
    [flagged]