APIPricingDocsBlogPartnersContact
Back to blog
Deep-dive

Why Open-Source ZKTeco Libraries Won't Scale (And What to Use Instead)

We tested 6 popular open-source ZKTeco libraries in production β€” pyzk, node-zklib, zklib, and more. Every one failed past 50 devices. Here's exactly what breaks and the cloud-first alternative.

PunchConnect TeamΒ·Mar 28, 2026Β·9 min read

Introduction

Every developer integrating ZKTeco biometric devices starts the same way: search GitHub, find an open-source library, write a script, watch it work on one device. Then they try to run it in production with 50+ devices, and everything falls apart. We know because we did exactly that β€” tested six of the most popular open-source ZKTeco libraries, pushed each one past its breaking point, and documented what failed, when, and why.

This article is the post-mortem. If you're evaluating open-source options for a biometric integration, read this before you commit four months of engineering time to something that won't survive its first week in production.

The 6 Libraries We Tested

We tested every actively-maintained open-source library for ZKTeco device communication. Here's the lineup:

1. pyzk (Python) β€” The most popular option. ~800 GitHub stars. Wraps the raw device protocol into Python classes. Last meaningful commit: varies, but the core protocol logic hasn't changed in years. Used by most tutorials and Stack Overflow answers.

2. node-zklib (JavaScript/Node.js) β€” The go-to for Node developers. ~400 stars. Provides async functions for connecting, fetching attendance, and managing users. Multiple forks exist because the original is semi-abandoned.

3. zklib (Python) β€” A lower-level alternative to pyzk. Fewer abstractions, more direct protocol access. Smaller community, but some developers prefer the control.

4. laravel-zkteco (PHP/Laravel) β€” A Laravel wrapper around the ZKTeco protocol. Popular in the PHP ecosystem, especially in Southeast Asia and Latin America where Laravel dominates. ~300 stars.

5. php-zkteco (PHP) β€” Standalone PHP implementation. No framework dependency. Used by developers building custom HR systems in plain PHP.

6. zk-protocol (Various) β€” Catch-all for the protocol documentation repos and minimal implementations. Not a single library but a family of reference implementations that developers fork and modify.

All six libraries do the same thing: connect to a ZKTeco device over the raw device protocol on a local network, authenticate, and read/write data. The API surface differs, but the architecture is identical.

What Works: Single Device on a LAN

Let's be fair to open source. For a single device on the same network as your script, these libraries work fine.

Here's pyzk fetching attendance records from one device:

That's clean. It works. You get data back in seconds. Same story with node-zklib:

```javascript
const ZKLib = require("node-zklib");
const zk = new ZKLib("192.168.1.201", 4370);

await zk.createSocket();
const attendances = await zk.getAttendances();
console.log(attendances);
await zk.disconnect();
```

If your entire use case is "one device, one office, run a cron job once a day" β€” stop reading. Open source is fine. Install pyzk, write a script, schedule it, move on.

But if you need to manage multiple devices, across multiple sites, running continuously β€” keep reading.

python
from zk import ZK
# Connect to a single device on LAN
zk = ZK('192.168.1.201', port=4370)
conn = zk.connect()
attendance = conn.get_attendance()
for record in attendance:
print(f"{record.user_id} punched at {record.timestamp}")
conn.disconnect()

Where It All Falls Apart

We deployed each library in a staging environment that mirrored our production setup: 50+ ZKTeco devices spread across multiple locations. Within 48 hours, every single one failed. Here are the five failure modes we documented.

1. Connection Management Collapses at Scale

Every open-source library manages connections the same way: one persistent socket per device. At 5 devices, that's 5 sockets. Fine. At 50 devices across 5 sites? That's 50 persistent connections your script needs to maintain simultaneously.

There's no connection pooling. No health checking. No automatic reconnection. The library gives you connect() and disconnect(), and everything in between is your problem.

python
# What your code looks like at scale with pyzk
devices = [
ZK('192.168.1.201', port=4370),
ZK('192.168.1.202', port=4370),
# ... 48 more devices, each needing its own connection
ZK('10.0.5.15', port=4370), # different subnet
ZK('172.16.0.8', port=4370), # different office entirely
]
# Now try to connect to all of them, keep them alive,
# detect disconnections, and handle timeouts.
# None of these libraries provide connection pooling.

2. No Retry Logic β€” Connection Drops Mean Data Loss

Network blips happen constantly in production. A switch reboots. Wi-Fi drops for 3 seconds. A device restarts after a firmware update. Every one of these events kills the socket connection.

Open-source libraries don't retry. The connection drops, the library throws an exception (or worse, hangs silently), and every attendance record punched during the downtime is lost until someone manually reconnects and pulls the backlog.

In our test, a single network interruption of 12 seconds caused 3 hours of missed records because no one noticed the connection was dead.

3. Blocking I/O β€” One Slow Device Freezes Everything

pyzk, php-zkteco, and laravel-zkteco all use blocking I/O. When you call get_attendance(), the entire thread waits for that device to respond. If one device is slow (bad network, heavy load, outdated firmware), every other device in your polling loop waits behind it.

node-zklib uses async sockets, which helps β€” but still lacks proper concurrency management for 50+ simultaneous connections.

python
# This looks innocent...
for device_ip in device_list:
zk = ZK(device_ip, port=4370, timeout=60)
conn = zk.connect()
records = conn.get_attendance() # ← blocks here
process(records)
conn.disconnect()
# ...but if device #3 takes 55 seconds to respond,
# devices #4 through #50 wait in line.
# A 5-minute polling cycle becomes a 45-minute one.

4. Memory Leaks After 48 Hours

Every library we tested leaked memory during long-running sessions. The pattern was consistent: memory usage climbed steadily from hour 0, hit a critical threshold around 36-48 hours, and either crashed or slowed to a crawl.

The root cause is the same across all libraries: socket buffers that don't get properly cleaned up, attendance records cached in memory without bounds, and event listeners that accumulate without being removed.

In production, this means you need to restart your integration service every 24-48 hours or build your own memory management wrapper. Most teams end up with a cron job that kills and restarts the process β€” which circles back to problem #2 (data loss during restart windows).

5. LAN-Only β€” No Cloud, No Multi-Site

This is the fundamental architectural problem. Every open-source library communicates with devices over the raw device protocol on a local network. Your script must be on the same network as the device. Period.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 420" fill="none" style="width:100%;max-width:800px;">
<text x="400" y="30" text-anchor="middle" fill="#94a3b8" font-size="17" font-weight="bold" font-family="system-ui">Open-Source Library Architecture: LAN-Only</text>

<!-- Office A -->
<rect x="30" y="60" width="340" height="150" rx="12" stroke="#64748b" stroke-width="1.5" fill="none" stroke-dasharray="6,4"/>
<text x="200" y="82" text-anchor="middle" fill="#64748b" font-size="14" font-family="system-ui">Office A β€” LAN 192.168.1.x</text>
<rect x="50" y="95" width="130" height="50" rx="8" stroke="#22d3ee" stroke-width="2" fill="none"/>
<text x="115" y="125" text-anchor="middle" fill="#22d3ee" font-size="14" font-family="system-ui">πŸ– Device 1</text>
<rect x="50" y="150" width="130" height="50" rx="8" stroke="#22d3ee" stroke-width="2" fill="none"/>
<text x="115" y="180" text-anchor="middle" fill="#22d3ee" font-size="14" font-family="system-ui">πŸ– Device 2</text>
<rect x="220" y="110" width="130" height="70" rx="8" stroke="#f87171" stroke-width="2" fill="none"/>
<text x="285" y="140" text-anchor="middle" fill="#f87171" font-size="14" font-family="system-ui">πŸ’» Local</text>
<text x="285" y="160" text-anchor="middle" fill="#f87171" font-size="14" font-family="system-ui">Server A</text>
<line x1="180" y1="120" x2="220" y2="135" stroke="#64748b" stroke-width="1.5"/>
<line x1="180" y1="175" x2="220" y2="155" stroke="#64748b" stroke-width="1.5"/>

<!-- Office B -->
<rect x="430" y="60" width="340" height="150" rx="12" stroke="#64748b" stroke-width="1.5" fill="none" stroke-dasharray="6,4"/>
<text x="600" y="82" text-anchor="middle" fill="#64748b" font-size="14" font-family="system-ui">Office B β€” LAN 10.0.5.x</text>
<rect x="450" y="95" width="130" height="50" rx="8" stroke="#22d3ee" stroke-width="2" fill="none"/>
<text x="515" y="125" text-anchor="middle" fill="#22d3ee" font-size="14" font-family="system-ui">πŸ– Device 3</text>
<rect x="450" y="150" width="130" height="50" rx="8" stroke="#22d3ee" stroke-width="2" fill="none"/>
<text x="515" y="180" text-anchor="middle" fill="#22d3ee" font-size="14" font-family="system-ui">πŸ– Device 4</text>
<rect x="620" y="110" width="130" height="70" rx="8" stroke="#f87171" stroke-width="2" fill="none"/>
<text x="685" y="140" text-anchor="middle" fill="#f87171" font-size="14" font-family="system-ui">πŸ’» Local</text>
<text x="685" y="160" text-anchor="middle" fill="#f87171" font-size="14" font-family="system-ui">Server B</text>
<line x1="580" y1="120" x2="620" y2="135" stroke="#64748b" stroke-width="1.5"/>
<line x1="580" y1="175" x2="620" y2="155" stroke="#64748b" stroke-width="1.5"/>

<!-- Cloud App -->
<rect x="270" y="280" width="260" height="70" rx="12" stroke="#a78bfa" stroke-width="2" fill="none"/>
<text x="400" y="310" text-anchor="middle" fill="#a78bfa" font-size="15" font-family="system-ui">☁️ Your Cloud App</text>
<text x="400" y="332" text-anchor="middle" fill="#a78bfa" font-size="14" font-family="system-ui">(AWS / Vercel / Railway)</text>

<!-- Blocked arrows -->
<line x1="285" y1="180" x2="350" y2="280" stroke="#f87171" stroke-width="2" stroke-dasharray="6,4"/>
<text x="290" y="240" fill="#f87171" font-size="20" font-family="system-ui">βœ•</text>
<line x1="685" y1="180" x2="450" y2="280" stroke="#f87171" stroke-width="2" stroke-dasharray="6,4"/>
<text x="570" y="240" fill="#f87171" font-size="20" font-family="system-ui">βœ•</text>

<!-- Problem label -->
<text x="400" y="400" text-anchor="middle" fill="#f87171" font-size="15" font-weight="bold" font-family="system-ui">⚠ Cloud app cannot reach devices behind office routers</text>
</svg>

That means:
- Multi-site deployments need a local server at every office
- Cloud applications (AWS, Vercel, Railway, Heroku) can't communicate with devices directly
- Remote offices require VPN tunnels or port forwarding β€” both fragile and hard to maintain
- Every new site means provisioning and maintaining another local server

If you're building a multi-tenant HR platform, a payroll SaaS, or anything cloud-hosted, open-source libraries are architecturally incompatible with your stack.

The Real Cost of "Free"

Open-source libraries cost $0 to install. They cost a fortune to run in production. Here's the math we did after our four-month integration attempt:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 380" fill="none" style="width:100%;max-width:800px;">
<text x="400" y="30" text-anchor="middle" fill="#94a3b8" font-size="17" font-weight="bold" font-family="system-ui">Total Cost of Ownership: Open-Source vs. PunchConnect</text>

<!-- Open Source Column -->
<rect x="40" y="55" width="330" height="300" rx="12" stroke="#f87171" stroke-width="2" fill="none"/>
<text x="205" y="82" text-anchor="middle" fill="#f87171" font-size="16" font-weight="bold" font-family="system-ui">Open-Source (50 Devices)</text>

<text x="65" y="115" fill="#94a3b8" font-size="14" font-family="system-ui">Protocol debugging</text>
<text x="340" y="115" text-anchor="end" fill="#f87171" font-size="14" font-weight="bold" font-family="system-ui">320 hrs Γ— $75 = $24,000</text>
<text x="65" y="145" fill="#94a3b8" font-size="14" font-family="system-ui">Connection management</text>
<text x="340" y="145" text-anchor="end" fill="#f87171" font-size="14" font-weight="bold" font-family="system-ui">160 hrs Γ— $75 = $12,000</text>
<text x="65" y="175" fill="#94a3b8" font-size="14" font-family="system-ui">Local server setup (5 sites)</text>
<text x="340" y="175" text-anchor="end" fill="#f87171" font-size="14" font-weight="bold" font-family="system-ui">$2,500 + $500/mo</text>
<text x="65" y="205" fill="#94a3b8" font-size="14" font-family="system-ui">VPN / networking</text>
<text x="340" y="205" text-anchor="end" fill="#f87171" font-size="14" font-weight="bold" font-family="system-ui">80 hrs Γ— $75 = $6,000</text>
<text x="65" y="235" fill="#94a3b8" font-size="14" font-family="system-ui">Ongoing maintenance</text>
<text x="340" y="235" text-anchor="end" fill="#f87171" font-size="14" font-weight="bold" font-family="system-ui">$1,500/month</text>
<line x1="60" y1="255" x2="350" y2="255" stroke="#64748b" stroke-width="1"/>
<text x="65" y="280" fill="#f87171" font-size="15" font-weight="bold" font-family="system-ui">Year 1 Total</text>
<text x="340" y="280" text-anchor="end" fill="#f87171" font-size="34" font-weight="bold" font-family="system-ui">$68,500</text>
<text x="205" y="310" text-anchor="middle" fill="#64748b" font-size="14" font-family="system-ui">+ ongoing $2,000/mo maintenance</text>
<text x="205" y="340" text-anchor="middle" fill="#f87171" font-size="14" font-family="system-ui">$1,370/device</text>

<!-- PunchConnect Column -->
<rect x="430" y="55" width="330" height="300" rx="12" stroke="#34d399" stroke-width="2" fill="none"/>
<text x="595" y="82" text-anchor="middle" fill="#34d399" font-size="16" font-weight="bold" font-family="system-ui">PunchConnect (50 Devices)</text>

<text x="455" y="115" fill="#94a3b8" font-size="14" font-family="system-ui">API integration</text>
<text x="730" y="115" text-anchor="end" fill="#34d399" font-size="14" font-weight="bold" font-family="system-ui">8 hrs Γ— $75 = $600</text>
<text x="455" y="145" fill="#94a3b8" font-size="14" font-family="system-ui">Device setup (5 min each)</text>
<text x="730" y="145" text-anchor="end" fill="#34d399" font-size="14" font-weight="bold" font-family="system-ui">~4 hrs = $300</text>
<text x="455" y="175" fill="#94a3b8" font-size="14" font-family="system-ui">Local servers needed</text>
<text x="730" y="175" text-anchor="end" fill="#34d399" font-size="14" font-weight="bold" font-family="system-ui">$0 (none)</text>
<text x="455" y="205" fill="#94a3b8" font-size="14" font-family="system-ui">VPN / networking</text>
<text x="730" y="205" text-anchor="end" fill="#34d399" font-size="14" font-weight="bold" font-family="system-ui">$0 (cloud-native)</text>
<text x="455" y="235" fill="#94a3b8" font-size="14" font-family="system-ui">Monthly service</text>
<text x="730" y="235" text-anchor="end" fill="#34d399" font-size="14" font-weight="bold" font-family="system-ui">Contact for pricing</text>
<line x1="450" y1="255" x2="740" y2="255" stroke="#64748b" stroke-width="1"/>
<text x="455" y="280" fill="#34d399" font-size="15" font-weight="bold" font-family="system-ui">Setup Cost</text>
<text x="730" y="280" text-anchor="end" fill="#34d399" font-size="34" font-weight="bold" font-family="system-ui">$900</text>
<text x="595" y="310" text-anchor="middle" fill="#64748b" font-size="14" font-family="system-ui">+ predictable monthly pricing</text>
<text x="595" y="340" text-anchor="middle" fill="#34d399" font-size="14" font-family="system-ui">Integration in hours, not months</text>
</svg>

The "free" library costs more in engineering time during the first month than a managed service costs in a year. We spent 4 months debugging raw device protocol edge cases β€” buffer overflows on specific firmware versions, timeout handling that differed between ZKTeco models, character encoding issues with employee names in non-Latin scripts.

Those 4 months of senior developer time could have built product features. Instead, they went into fighting a communication protocol that was never designed for the use case we were forcing on it.

The Cloud-First Alternative

PunchConnect takes a fundamentally different approach. Instead of your code connecting to devices, devices connect to PunchConnect's cloud. You configure the device in the dashboard, and it starts sending data. Your application talks to a REST API β€” not to devices.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 340" fill="none" style="width:100%;max-width:800px;">
<text x="400" y="30" text-anchor="middle" fill="#94a3b8" font-size="17" font-weight="bold" font-family="system-ui">PunchConnect Cloud Architecture</text>

<!-- Devices -->
<rect x="30" y="70" width="150" height="55" rx="8" stroke="#22d3ee" stroke-width="2" fill="none"/>
<text x="105" y="103" text-anchor="middle" fill="#22d3ee" font-size="14" font-family="system-ui">πŸ– Office A (12)</text>
<rect x="30" y="140" width="150" height="55" rx="8" stroke="#22d3ee" stroke-width="2" fill="none"/>
<text x="105" y="173" text-anchor="middle" fill="#22d3ee" font-size="14" font-family="system-ui">πŸ– Office B (8)</text>
<rect x="30" y="210" width="150" height="55" rx="8" stroke="#22d3ee" stroke-width="2" fill="none"/>
<text x="105" y="243" text-anchor="middle" fill="#22d3ee" font-size="14" font-family="system-ui">πŸ– Office C (30)</text>

<!-- Arrows to cloud -->
<line x1="180" y1="97" x2="300" y2="155" stroke="#34d399" stroke-width="2" marker-end="url(#cloudArr)"/>
<line x1="180" y1="167" x2="300" y2="167" stroke="#34d399" stroke-width="2" marker-end="url(#cloudArr)"/>
<line x1="180" y1="237" x2="300" y2="180" stroke="#34d399" stroke-width="2" marker-end="url(#cloudArr)"/>

<!-- PunchConnect Cloud -->
<rect x="300" y="115" width="200" height="110" rx="14" stroke="#34d399" stroke-width="2.5" fill="none"/>
<text x="400" y="155" text-anchor="middle" fill="#34d399" font-size="17" font-weight="bold" font-family="system-ui">PunchConnect</text>
<text x="400" y="178" text-anchor="middle" fill="#34d399" font-size="14" font-family="system-ui">Cloud Engine</text>
<text x="400" y="198" text-anchor="middle" fill="#64748b" font-size="14" font-family="system-ui">50 devices β€’ 24,000+ employees</text>

<!-- Arrow to your app -->
<line x1="500" y1="170" x2="590" y2="170" stroke="#a78bfa" stroke-width="2" marker-end="url(#appArr)"/>
<text x="545" y="158" text-anchor="middle" fill="#a78bfa" font-size="13" font-family="system-ui">REST API</text>

<!-- Your App -->
<rect x="590" y="120" width="180" height="100" rx="12" stroke="#a78bfa" stroke-width="2" fill="none"/>
<text x="680" y="155" text-anchor="middle" fill="#a78bfa" font-size="15" font-family="system-ui">πŸ’» Your App</text>
<text x="680" y="178" text-anchor="middle" fill="#64748b" font-size="14" font-family="system-ui">Any language</text>
<text x="680" y="198" text-anchor="middle" fill="#64748b" font-size="14" font-family="system-ui">Any hosting</text>

<!-- Bottom stats -->
<text x="130" y="310" text-anchor="middle" fill="#22d3ee" font-size="34" font-weight="bold" font-family="system-ui">50+</text>
<text x="130" y="332" text-anchor="middle" fill="#64748b" font-size="14" font-family="system-ui">sites supported</text>
<text x="400" y="310" text-anchor="middle" fill="#34d399" font-size="34" font-weight="bold" font-family="system-ui">5 min</text>
<text x="400" y="332" text-anchor="middle" fill="#64748b" font-size="14" font-family="system-ui">per device setup</text>
<text x="670" y="310" text-anchor="middle" fill="#a78bfa" font-size="34" font-weight="bold" font-family="system-ui">95%</text>
<text x="670" y="332" text-anchor="middle" fill="#64748b" font-size="14" font-family="system-ui">uptime guaranteed</text>

<defs>
<marker id="cloudArr" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#34d399"/></marker>
<marker id="appArr" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#a78bfa"/></marker>
</defs>
</svg>

No local servers. No static IPs. No VPN tunnels. No raw protocol debugging. You configure the device in the dashboard, and your code calls a REST API.

Here's the same attendance fetch β€” through PunchConnect instead of a raw device connection:

That's it. All 50 devices, all sites, one API call. No sockets to manage, no connections to monitor, no memory leaks to debug. PunchConnect handles the device communication β€” you handle your business logic.

What this looks like in practice:
- Setup: Configure the device's server address in its menu to point to PunchConnect. Takes about 5 minutes per device.
- Integration: Call the REST API from any language, any framework, any hosting provider. Standard HTTP.
- Monitoring: Device status, connectivity, and last-seen timestamps available through the dashboard and API.
- Scale: Adding a new device at a new site is the same 5-minute process. No infrastructure changes needed.

PunchConnect runs in production with 24,000+ active employees across 50+ sites on AgriWise. This isn't a proof of concept β€” it's battle-tested infrastructure.

python
import requests
# No device connection needed β€” just call the API
response = requests.get(
"https://api.punchconnect.com/v1/attendance",
headers={"Authorization": "Bearer YOUR_API_KEY"},
params={"from": "2026-03-01", "to": "2026-03-28"}
)
for record in response.json()["data"]:
print(f"{record['employeeId']} punched at {record['timestamp']}")

When Open-Source Is Still the Right Choice

We're not here to trash open-source tools. They have legitimate use cases:

- Learning the protocol. If you want to understand how ZKTeco devices communicate at a low level, pyzk and zklib are excellent educational resources. Read the source code.
- Lab testing. Evaluating a new ZKTeco model? Open-source libraries are great for quickly checking compatibility, reading device info, and testing features before committing to a deployment.
- 1-3 devices, single office, dedicated developer. If you have a small deployment, a local server you already maintain, and a developer who will own the integration long-term β€” open source can work. Just know the maintenance cost going in.
- Contributing to the ecosystem. The open-source ZKTeco community has built something valuable. If you have protocol expertise, contributing back makes the whole ecosystem better.

The breaking point is clear: the moment you need multi-site, cloud-hosted, or more than a handful of devices β€” you've outgrown what these libraries were designed for.

FAQ

Can I use pyzk or node-zklib in production?

For small deployments (1-5 devices, single site, same LAN), yes β€” with caveats. You'll need to build your own retry logic, connection monitoring, and restart mechanisms. For anything beyond that, the maintenance burden grows faster than your deployment.

What happens when open-source libraries lose connection to a device?

Most throw an exception or hang indefinitely. None automatically reconnect. Records punched during downtime are stored on the device but aren't fetched until someone manually restarts the connection and pulls the backlog. Some libraries don't track which records have already been synced, leading to duplicates.

Does PunchConnect work with all ZKTeco models?

PunchConnect supports all ZKTeco models manufactured after 2015 that have network connectivity (Ethernet or Wi-Fi). This covers the vast majority of devices in active deployment: SpeedFace, ProFace, MultiBio, uFace, K-series, and UA-series. If you have a specific model, check compatibility or reach out β€” we'll confirm it.

How long does it take to migrate from open-source libraries to PunchConnect?

Most teams complete the migration in 1-2 days. The device-side change takes 5 minutes per device (configuring the server address in the dashboard). The code-side change depends on your existing integration, but replacing raw socket calls with REST API calls is straightforward. We've seen teams migrate 50+ devices in a single afternoon.

Is PunchConnect itself open-source?

No. PunchConnect is a managed cloud service. The device communication layer β€” the part that breaks in open-source libraries β€” is exactly what PunchConnect handles for you. The REST API you interact with is documented and uses standard HTTP conventions that work with any language or framework.

---

Keep Reading

- node-zklib Cloud Alternative: Connect ZKTeco Devices Without Local Servers β€” Specific migration guide from node-zklib to PunchConnect
- Turn Any ZKTeco Device Into a REST API β€” Complete guide to getting attendance data through HTTP endpoints
- Biometric Attendance Without a Static IP β€” How cloud-connected devices eliminate the static IP requirement
- ZKTeco Push Protocol Explained β€” Deep dive into push vs. pull mode and why it matters for cloud deployments

Related articles

Why Open-Source ZKTeco Libraries Won't Scale (And What to Use Instead) | PunchConnect