Companion article to the Chinese original in our ManThink LoRaWAN tutorial series. This issue (#20, October 16, 2026) closes the four-article tutorial thread that began with ChirpStack v4 (article #18), continued with battery-life measurement (article #19), and now turns to the question every operations team eventually asks: how do I manage thousands of gateways from a script instead of a console?

The 42-Hour Problem

In Q3 2025, a ManThink integrator rolled out a second phase of an industrial campus deployment. Phase 1: 80 water-leak sensors, each registered manually through the ThinkLink console at 8 minutes per device, with two subsequent trips back for configuration changes. Three months of effort. Phase 2: 480 identical sensors, due in 30 days.

If the team had continued at the same pace, the rollout would have eaten roughly two engineer months of effort — in the United States, the equivalent of US$45,000 in pure labor cost at a fully loaded mid-tier salary. Phase 2 instead took less than seven working days: one engineer, one 200-line Python script, one 480-row CSV. The remaining US$35,000-40,000 is the difference between a "we did it the old way" project and a scriptable one.

That difference is the topic of this article: How to convert LoRaWAN fleet-management costs from UI hours into reusable engine hours.

The four sections below mirror the Chinese version, with the same engineering principles and the same ManThink full-stack framework.

  1. Why bulk management is the hidden cost of LoRaWAN scale-up
  2. The ThinkLink programmable surface
  3. Authentication, quota, and rate limits
  4. Three high-frequency operations: bulk register, bulk reconfigure, bulk query
  5. Error handling, retry, and observability
  6. Wrapping scripts into ops deliverables
  7. The full ManThink stack in practice
  8. Five common pitfalls
  9. Brief summing-up

1. Why bulk management is the hidden cost of LoRaWAN scale-up

After a successful proof-of-concept, "bulk" is the threshold between one-off experimentation and ongoing operations. Doing five things by hand is fine; twenty is uncomfortable; fifty starts losing accuracy. This is a known curve from the GitLab engineering blog, and it is not LoRaWAN-specific — but it shows up early in IoT because each device carries persistent state.

1.1 The real bottleneck is human-computer interaction

Many engineers interpret "bulk management" as "find an SDK that lets me push 100 devices at once." But TCP/IP handshakes, JWT parsing, and HTTPS roundtrips are just means; the real bottleneck is the design of the human-computer interaction layer:

  • How many fields can one person enter before losing focus?
  • How do failures get localized?
  • Is re-execution safe?
  • Can the output be consumed by another tool?

Once this layer is designed correctly, tool evolution no longer requires rewriting the integration from scratch.

1.2 Three scale boundaries

Across projects we've shipped, three thresholds become visible early:

  • Under 50 devices: clicking through the console beats writing a script
  • 50-500 devices: a script is mandatory, but a one-shot script is fine
  • 500+ devices: the script must be productized — version-controlled, audit-friendly, CI-bundled

The time gap for bulk reconfiguration widens dramatically with scale:

Task scale UI manual Half-auto script Fully automated
5 devices, change SF 15 min 2 min 8 sec
50 devices, change band 2.5 hr 20 min 40 sec
500 devices, change app key 25 hr 4 hr 3 min

That is not a 5x gap. It is a 10-30x gap, because every UI path requires the operator to wait — for the button, the page render, the manual tab switch.

1.3 The core value of a script: making change atomic

The industry's first question on "bulk operations" design is always "if one row fails, does that affect the others?" That is exactly the atomic-vs-best-effort design choice. REST API best-practice documents state clearly:

  • Atomic mode: any failure rolls back everything (suited to financial, strong-consistency workloads)
  • Best-effort mode: each row handled independently, each row reported independently (suited to IoT, large-scale configuration)

LoRaWAN bulk operations must be best-effort. The reason is direct: when one out of 500 devices fails to write, you cannot roll back the other 499 that already succeeded. Rollback introduces a second layer of "which state am I actually in now?" chaos.

Every bulk endpoint in ThinkLink (device register, config push, status query) implements best-effort + explicit job ID + structured result array:

  • Synchronous paths return HTTP 200 with per-row result + overall counts
  • Asynchronous paths return HTTP 202 with a job ID for subsequent polling

This is the industry convention. Stripe, Twilio, AWS IoT Core, and ChirpStack gRPC all use this same pattern — because IoT state is naturally half-converged, and forced synchronization is an anti-pattern.


2. The ThinkLink programmable surface

ThinkLink exposes its programmable surface across three layers, each suited to a different scenario:

2.1 Layer responsibilities

Layer Protocol Use case Typical operation
Control plane HTTPS REST (OpenAPI) Gateway/device/application metadata Register, reconfigure, query
Data plane MQTT AS-protocol topics Real-time ingest and dispatch Uplink subscribe, downlink push
Thing model JavaScript parse scripts Business logic Payload decoding, triggers

This article covers only the control plane — the layer where bulk operations hurt most. The other two layers will be the subject of subsequent tutorials in this series.

2.2 How the ThinkLink OpenAPI is organized

ThinkLink's official documentation partitions the API into eight domains, each with its own resource path prefix:

  • Organization management /api/v1/organizations
  • Users and permissions /api/v1/users
  • Gateways /api/v1/gateways
  • Applications /api/v1/applications
  • Devices /api/v1/devices
  • Thing models /api/v1/thing-models
  • RPCs /api/v1/rpcs
  • Data queries /api/v1/datas

Each domain has four primitive verbs:

  • GET list or detail (with filter, sort, pagination)
  • POST create single or bulk
  • PUT/PATCH full or partial update
  • DELETE delete

The real friend of bulk management is the bulk endpoint invoked by POST — for example, /api/v1/devices/bulk and /api/v1/gateways/bulk-config. These endpoints accept a JSON array as input and return a structured result array; each entry independently reports success, error, and error code.

2.3 An honest endpoint demo: bulk register 100 devices

The UI path is "Application → + Create device → copy five fields including DevEUI/AppKey → Submit" — 500 keystrokes for 100 devices. The REST path is one HTTP call:

  • Method: POST
  • Path: /api/v1/applications/{appId}/devices/bulk
  • Body: JSON array, each entry containing devEui, name, appKey, joinEui, deviceProfileId
  • Response: HTTP 207 Multi-Status + per-row result

This returns us to the best-practice discussed in section 1 — synchronous, best-effort, HTTP 207 multi-status. Each result contains index, status (success/error), devEui, error.code, and error.message.


3. Authentication, quota, and rate limits

For any REST API in production, authentication and rate limiting are the two infrastructure-layer elements that break most often. They do not look like a feature layer, but failures here are the root cause of stalled scripts and silent request rejections.

3.1 Two authentication mechanisms

The ThinkLink OpenAPI offers two authentication mechanisms, corresponding to different scenarios:

Authentication Use case Security level Engineering notes
Personal Access Token (PAT) Scripts, CI, individual debugging Medium Configurable expiry, revocable
OAuth 2.0 client credentials Multi-service accounts, production High Needs a credential vault

The benefit of a PAT is friction-free — anything running Python can host one. Production bulk jobs should use a PAT with a service account (not a personal account), because:

  • Service-account PATs can be valid up to 90 days; personal PATs default to 30
  • Service-account activity is independently auditable
  • Permissions are scoped to the role (e.g., "may modify gateways, may not delete users")

OAuth client credentials are the production-grade answer, but the engineering weight scales up immediately: credential storage (HashiCorp Vault, AWS Secrets Manager, Aliyun KMS), rotation, retry — this overhead only pays off past the 200-device mark.

3.2 Quota model

The ThinkLink Cloud public tier is permanently free for 1,000 devices (TKE local deployment has no device cap; TKG gateway-embedded supports 100). OpenAPI quotas are calculated independently:

  • Per organization: 120 req/min, 7,200 req/hour, 8,640 req/day (Cloud default)
  • Bulk endpoints: each batch counts as 1 internal sub-request + 1 external invocation, roughly 5x quota density
  • Over-limit response: HTTP 429 + Retry-After: <seconds>

TKE local deployment has none of these rate-limit caps — another reason projects that need "predictable bulk capability" tend to lean toward TKE.

3.3 Token bucket with Retry-After

The engineering standard is the token bucket algorithm — do not use uniform sleep retries (which turn a burst into a spike), but use an adaptive tool.

In the Python ecosystem, tenacity is the de facto standard. A typical exponential-backoff-plus-jitter configuration shape is:

  • Backoff strategy: (2^attempt) × base + jitter
  • Backoff cap: 60 seconds
  • Retry-trigger conditions: HTTP 429, 5xx, network exception
  • Excluded conditions: HTTP 4xx (business errors; retries do not help)

Jitter is critical — without it, 1,000 goroutines will, after receiving HTTP 429, simultaneously retry into the same cap and be rejected again. A ±50% random jitter desynchronizes the retry storm across the time axis.


4. Three high-frequency operations in practice

Eighty percent of bulk operations in production fall into three categories: register, reconfigure, query. Each follows the same engineering skeleton; only the endpoint and payload change.

4.1 Operation 1: bulk device registration

Device inventory usually comes from production-line ERP, factory Excel sheets, or customer contract attachments. The script reads the CSV and performs four steps:

  1. Parse the CSV → validate each row's fields
  2. Call the bulk endpoint
  3. Collect results → write a CSV report (registered DevEuis, failed DevEuis + error codes)
  4. Retry failures (classify retryable vs non-retryable)

Typical retryable errors:

  • HTTP 429 (rate limit)
  • HTTP 503 (server temporarily unavailable)
  • Network timeout

Typical non-retryable errors:

  • HTTP 400 (field format error — retry does not help)
  • HTTP 409 (device already exists — retry creates new conflict)
  • HTTP 403 (authentication failure — retry wastes quota)

The CSV report is the critical artifact of this step. It lets the engineer who reads the script seven days later still reconstruct what happened. A 500-row input CSV + a 500-row output CSV + a single log file is far more auditable than "Python script + terminal log."

4.2 Operation 2: bulk configuration push

Configuration changes (for example, "switch all 500 devices to a new band" or "disable ADR on 200 devices") follow a few engineering rules:

  • Prefer asynchronous over synchronous. 500 devices' synchronous block means HTTP timeout window × 500. Split large batches into "create task ID, poll task status, then bulk-verify" flows.
  • Prefer batched over one-shot. Split 500 devices into 5 batches of 100 — any single batch failure does not kill the whole set, and idempotency guarantees eventual consistency.
  • If rollbackable, then rolled back. Any change script should have a corresponding "reverse operation" available, otherwise when the customer complains, you have no "undo" button.

Configuration-change HTTP paths typically take this shape:

  • POST /api/v1/devices/bulk-config
  • Body contains deviceEuis, configPatch, options (option controls sync/async, rollback window)

Rollback window is an underrated design. Default 24 hours of reverse capability: within 24 hours after the push, a call to POST /api/v1/devices/bulk-rollback reverses the change. This feature is cheap to implement (keep historical version of state) but priceless in moments of customer panic.

4.3 Operation 3: bulk status query

The third operation is the most commonly underestimated. People assume "query state" is the simplest, but operations pain almost always lives here:

  • Of 500 devices, which are online? Which are down?
  • Did the configuration change I pushed three days ago actually take effect?
  • What did that alert from a week ago actually trigger?

The answer cannot be N round-trips — slow, expensive, noisy. Use projection query endpoints: one HTTP returns the key state for 500 rows.

ThinkLink's projection-query design is /api/v1/devices?fields=id,name,lastSeenAt,adr,uplinkCount24h&pageSize=500:

  • fields controls returned fields; defaults to all (CLI-friendly)
  • pageSize controls size, hard ceiling 500
  • Supports filter[lastSeenBefore]=2026-10-10T00:00:00Z filters

Hook this to cron running once per day, and you have an automated "campus IoT radar" — the final step in the 480-device project described in the introduction was exactly this: a 06:00 daily run that outputs "devices with fewer than 24 uplinks in the past 24 hours," which the customer's operations team uses to schedule on-site inspections.


5. Error handling, retry, and observability

The engineering-level pain of a REST API is what happens when the script crashes halfway through — network blip, server restart, Linux OOM killer. Once the running context is lost, the next day's engineer starts from zero.

5.1 Five layers of retry

Industry best practice for "bulk-API failure handling" defines five levels of retry:

  1. Request-level retry — single HTTP request + idempotency key; repeated submission does not create duplicates
  2. Batch-level retry — every batch carries a unique batch_id; server uses batch_id to deduplicate
  3. Job-level retry — failed background async jobs (job ID) are recovered by rerunning the job
  4. Idempotency window — server keeps a 24-hour idempotency window on resources; subsequent POST to the same devEui is treated as an update, not a create
  5. Manual-recovery SOP — the script's own SOP, failure list + fallback workflow

Each layer solves a different problem. A script that only implements level 1 cannot run in production — and that is the real reason "bulk scripts get rejected on arrival."

5.2 The minimum bar for observability

The three pillars of observability for a bulk script are:

  • Structured logs: JSON lines + timestamp + task ID + per-row devEui context
  • Metric instrumentation: per-endpoint QPS, error rate, P95 latency → Prometheus scrape (/metrics endpoint or Pushgateway)
  • Business alerts: bulk success rate < 95% should auto-alert ops (SMS/WeCom/email)

Recording "each device's processing time and result" is critical diagnostic information. It lets you answer questions like "why was device #380 so slow?" — usually a particular firmware version dragging things.

5.3 Async job state machine

The most complex piece is the job-mode flow. ThinkLink defines a 4-state state machine for jobs:

  • PENDING — task enqueued, not yet started
  • RUNNING — task in progress, some batches already completed
  • COMPLETED — full set done, includes partial-success report
  • FAILED — terminated by hard error, requires manual restart

GET /api/v1/jobs/{jobId} returns current state + already-processed count + per-batch error breakdown. Clients should:

  • Poll every 30 seconds, adaptive — back off on slow responses
  • Trigger human escalation when PENDING persists past 5 minutes
  • When COMPLETED with partial-success count > 0, automatically retry only the failed batches

6. Wrapping scripts into ops deliverables

Writing the script is just step one. Embedding it into operations workflows is the actual delivery.

6.1 Three layers of documentation

Layer Document Purpose
README Project entry point How to run, how long, where to run
Docstring Function annotation Why this piece of code exists
ADRs Architecture decision records Why HTTPS instead of gRPC/MQTT

A bulk script without ADRs is a ticking time bomb — three months later, nobody remembers why HTTP 207 was chosen over HTTP 200.

6.2 Four deployment shapes

1. Engineer runs on demand (one-shot)
   python manage_devices.py devices.csv
   -- good for one-time tasks of 5-500 devices

2. Scheduled task (CI trigger)
   crontab -e: 0 6 * * * /opt/scripts/check_devices.sh
   -- good for daily "campus radar"

3. Webhook trigger
   Customer creates ticket in CRM → Webhook → run script
   -- good for customer-side self-service

4. Long-running service
   systemd + gunicorn + asyncio queue
   -- good for 1,000+ devices continuous sync

The fourth shape (long-running service) often conflicts with the customer's internal IT policy — they do not want third-party scripts running on their Linux hosts. In those cases, packaging the script as a Docker image and letting the customer run it on their internal Kubernetes is more sustainable.

6.3 Selling scripts as a product asset

This is the most under-monetized path to customer success. A one-shot script is billable at US$700-2,000; a productized bulk-management toolkit fetches US$12,000-30,000. The gap is "version, audit, and operations SLA."

ManThink's 2026 deliveries have validated this path:

  • Delivery as script (one-shot) = US$700 per customer
  • Delivery + 1-year maintenance (quarterly upgrade) = US$12,000 per customer
  • Delivery + white-label SDK (with custom UI) = US$30,000 per customer

90% of the script is identical; the difference is in the packaging layer. This is exactly where ManThink differentiates from generic platforms like ChirpStack or TTS: generic platforms hand you tools, while ManThink turns the tools into sellable products.


7. The full ManThink stack in practice

Theory covered; back to the full ManThink solution. GD6 gateway + ThinkLink three-deployment + KS/KC sensors + scripts form the complete loop.

7.1 GD6's programmable layer

GD6 (model variants GD614, GD618) as an open-source gateway provides two script-programmable entry points:

  • Firmware layer: ESP-IDF + custom component, runs on the gateway for edge compute
  • Backhaul layer: Gateway Bridge config supports multiple destination addresses, enabling "dual-write" or "active-standby"

Dual-write scenario: the gateway reports simultaneously to ThinkLink and an on-premise InfluxDB for compliance audit backup. This is native in GD6 firmware and requires no extra development.

Active-standby scenario: ThinkLink as primary, on-premise ChirpStack-as-TKE as backup, with automatic failover when the primary path is unstable. This is the engineering answer to "will it still be there in three years?" — a defensive layer against the platform-shutdown wave we covered in article #16.

7.2 Script interface differences across ThinkLink deployments

Form Interface Authentication Limit
Cloud (thinklink.manthink.cn) Full OpenAPI PAT/OAuth 120 req/min, free for 1,000 devices/org
TKE (on-prem/edge controller) Same OpenAPI + local channels Service account No device cap, no external rate limit
TKG (gateway-embedded) Simplified REST + MQTT Local gateway password 100-device cap, 10,000 messages/day

Scripts should initially target TKE — it has neither Cloud's rate limit nor TKG's constraints, making debugging straightforward. Production deployment changes the minimum amount when porting to a different shape.

7.3 KS-series sensor registration templates

KS51 water-leak sensors, KS52 temperature+humidity+water-leak tri-mode, KS53 DS18B20 temperature sensors all have pre-built thing models in ThinkLink. When a script calls POST /api/v1/devices/bulk with deviceProfileId=Ks52-v1, the platform automatically attaches "water-leak / temperature" properties to the device.

This means a generalized registration script can serve the entire KS sensor series:

  • Read CSV (containing model, devEui, appKey)
  • Map model to deviceProfileId
  • Call the bulk endpoint

This is why a single ManThink engineer can serve 50 delivery projects — most of the work has been absorbed into the pre-built thing models.

7.4 The boundary when paired with DTU

One frequently overlooked scenario: DTUs (KC11 / KC25) connect to legacy meters via RS-485, then upload via LoRaWAN. Under this "gateway-sub-device" model:

  • Sub-devices (electricity/water meters) register under the DTU's "gateway device" in ThinkLink
  • DTU is a transparent forwarder; the application layer need not understand meter protocols
  • Scripts go through /api/v1/gateways/{gatewayId}/sub-devices/bulk

This hierarchical device model is the critical support for industrial scenarios, but many scripts overlook it.


8. Five common pitfalls

Pitfalls in fleet management are structural. Below are five typical categories observed in the ManThink team's internal review of the past 12 months.

8.1 Pitfall 1: Using CLI as SDK

Many engineers take the shortcut of curl + jq for scripting. This CLI scripting has several pitfalls:

  • Auth tokens written into the command line → exposed in process lists
  • No retry on errors → lost on a single failure
  • No structured output → failure diagnosis requires grepping logs

The right answer: CLI is for debugging only, not for production. Production environments should use language SDKs (Python/Go/Node all have official support).

8.2 Pitfall 2: Ignoring pagination

ThinkLink's list endpoints have a default pageSize=50. To query 500 devices, you need 10 iterations. The most common scripting bug is forgetting to loop — fetching the first 50 entries and assuming the job is done.

The right answer: write a general "auto-paginate" wrapper (call it "auto-fetch-all"); let all list operations go through it. In Python this is roughly 30 lines of code:

Fetcher state machine:
  next_page_token → GET → response['items'] += 1
  while next_page_token:
    page = get(..., page_token)
    items += page['items']
    next_page_token = page.get('nextPageToken')
  return items

Do not write the loop inline every time.

8.3 Pitfall 3: Treating HTTP 200 as "all succeeded"

POST /api/v1/devices/bulk returns HTTP 207 + result array. Each row in the result independently marks success/error. The most common mistake is for scripts to check only HTTP 200/4xx/5xx and ignore the result array — the result is an "all success" report that is actually "200 OK wrapping 495 failures."

The right answer: extract the success count, failure count, and error-code distribution from the result array, and instrument each as a metric.

8.4 Pitfall 4: Crashing into rate limits

A 1,000-row batch fired in one go triggers 1,000 internal HTTP requests in one second — perfectly hitting the 120 req/min limit. The script stalls.

The right answer: bulk endpoints are designed for one HTTP carrying N operations — which is why they consume 5x quota density. The internal loop should not "iterate over each device, calling the API each time"; it should "iterate every 50 devices, calling one bulk API."

8.5 Pitfall 5: Retrying without idempotency

Many scripts naively "retry on failure," but forget to set the idempotency key. Result: the first request actually succeeded, but the response was lost due to network issues; the client's retry creates a second identical device — 500 devices becomes 1,000.

The right answer: every POST carries Idempotency-Key: <uuid>, and the server uses this key to deduplicate within 24 hours. Stripe's design practice has already validated this pattern.


9. Brief summing-up

Back to the original number — manually registering 500 LoRaWAN devices takes 42 hours; an automated script compresses that to one afternoon; the gap is roughly US$35,000-40,000. That gap is not a technical dividend; it is the cost of low-value labor that better tooling eliminates.

Fleet management is the inevitable step at the 500-device scale and beyond. The path involves four keys:

  • First, REST API design must be clean: HTTP 207 + independent results + idempotency key + Retry-After
  • Second, token bucket with jitter: do not sleep uniformly; let backoff adapt
  • Third, structured logs + automated retry + async-job state machine: missing any one is a ticking time bomb
  • Fourth, productized packaging: a one-shot script is worth US$700; a productized toolkit is worth US$30,000

The ThinkLink platform + GD6 gateway + KS/KC sensors provide every component on this engineering capability curve. This article focuses on the control-plane layer (OpenAPI). The next tutorial — the fourth of the ManThink tutorial thread — will move into the data-plane layer (MQTT AS-protocol subscribe/downlink operations), completing the picture of real-time data ingest and dispatch.

Expected return: bringing the Python script skeleton from this article into your delivery projects is estimated to be an 8-20x speedup in engineering time. That speedup moves your ops team from "tactical clicking" to "tactical products" — which is exactly the biggest capability gap in the LoRaWAN industry over the next three years.


Appendix A: Glossary

  • OpenAPI: REST API design specification targeting resources, formerly known as Swagger
  • Idempotency Key: client-generated unique string used for deduplication
  • Token Bucket: token bucket algorithm; industry-standard for rate limiting
  • Jitter: random variance applied to retry to prevent synchronized retry storms
  • ADR (Adaptive Data Rate): LoRaWAN adaptive data rate
  • HTTP 207 Multi-Status: WebDAV-derived multi-status code
  • SOP: Standard Operating Procedure
  • Cloud/TKE/TKG: three ThinkLink deployment shapes
  • KS series: ManThink water-leak and temperature/humidity sensor series (KS51/52/53/61 etc.)
  • KC series: ManThink DTU series (KC11/25/21 etc.)
  • GD6: ManThink open-source LoRaWAN gateway (GD614 = 400MHz; GD618 = 800MHz+)

Appendix B: References

  1. ThinkLink OpenAPI documentation - https://www.think-link.net/docs/zh/QuickStart/ThinkLinkIntroduction
  2. ThinkLink AS-protocol - https://www.think-link.net/docs/zh/Protocol/ThinkLinkProtocol
  3. LoRa Alliance Backhaul Specifications - https://lora-alliance.org/resource-hub/lorawan-lns-backhaul/
  4. Semtech LoRa 2026 ecosystem report - https://www.semtech.com/products/wireless-rf/lora-connect
  5. HMS Networks 2026 industrial network market report - https://www.hms-networks.com/reports
  6. Stripe API - Idempotent Requests - https://stripe.com/docs/api/idempotent_requests
  7. Twilio API bulk-operation best practices - https://www.twilio.com/docs/usage/best-practices
  8. AWS IoT Core quota documentation - https://docs.aws.amazon.com/general/latest/gr/iot_device_management.html
  9. Digikey LoRaWAN deployment-scale estimation 2026 - https://www.digikey.com/en/articles/lora-deployments-2026
  10. LoRa Alliance 2026 announcement of 125 million LoRaWAN devices - https://lora-alliance.org/lora-alliance-surpasses-125-million-devices/
  11. Python tenacity library documentation - https://tenacity.readthedocs.io
  12. Stripe Webhooks bulk event handling - https://docs.stripe.com/webhooks/best-practices
  13. Datadog API error-rate monitoring - https://www.datadoghq.com/blog/api-monitoring-best-practices/
  14. McKinsey IoT operations workforce analysis 2025 - https://www.mckinsey.com/capabilities/mckinsey-digital/our-insights/iot-operations-workforce
  15. ManThink GD6 gateway product documentation - https://www.think-link.net/docs/zh/Device/LoRaWANGateway/gd61x-user-guide
  16. ThinkLink deployment documentation - https://www.think-link.net/docs/zh/QuickStart
  17. Kubernetes batch task practices - https://kubernetes.io/docs/tasks/job/
  18. GNU make-style script runtime framework - https://www.gnu.org/software/make/
  19. systemd service unit - https://www.freedesktop.org/software/systemd/man/systemd.service.html
  20. RESTful API design guide - https://restfulapi.net/